feat: Update website, API, and infra (#164)

>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.

>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI 

>TODO:
>- Add a README
>- Add an SST docs

Edit:

- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
This commit is contained in:
Wanjohi
2025-02-11 12:26:35 +03:00
committed by GitHub
parent 93327bdf1a
commit 060718d8b0
139 changed files with 5814 additions and 5049 deletions

View File

@@ -1,19 +1,22 @@
import { component$, Slot } from "@builder.io/qwik";
// import SansNormal from "@fontsource/geist-sans/400.css?inline"
//font-sans or font-body
import "@fontsource/geist-sans/400.css"
import "@fontsource/geist-sans/500.css"
import "@fontsource/geist-sans/600.css"
import "@fontsource/geist-sans/700.css"
import "@fontsource/bricolage-grotesque/500.css"
import "@fontsource/bricolage-grotesque/700.css"
import "@fontsource/bricolage-grotesque/800.css"
// font mono
import "@fontsource/geist-mono/400.css"
import "@fontsource/geist-mono/700.css"
//font-mona
import "@fontsource-variable/mona-sans"
// import "@fontsource/mona-sans/500.css"
// import "@fontsource/mona-sans/600.css"
// import "@fontsource/mona-sans/700.css"
// import "@fontsource/mona-sans/800.css"
//font-bricolage
import '@fontsource-variable/bricolage-grotesque';
export const Fonts = component$(() => {
// useStyles$(SansNormal);
return <Slot />;
});

View File

@@ -1,134 +0,0 @@
/* eslint-disable qwik/jsx-img */
import { component$ } from "@builder.io/qwik";
import { Link } from "@builder.io/qwik-city";
import { MotionComponent, transition } from "@nestri/ui/react"
import { FooterBanner } from "./footer-banner";
const socialMedia = [
{
link: "https://github.com/nestriness/nestri",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .999c-6.074 0-11 5.05-11 11.278c0 4.983 3.152 9.21 7.523 10.702c.55.104.727-.246.727-.543v-2.1c-3.06.683-3.697-1.33-3.697-1.33c-.5-1.304-1.222-1.65-1.222-1.65c-.998-.7.076-.686.076-.686c1.105.08 1.686 1.163 1.686 1.163c.98 1.724 2.573 1.226 3.201.937c.098-.728.383-1.226.698-1.508c-2.442-.286-5.01-1.253-5.01-5.574c0-1.232.429-2.237 1.132-3.027c-.114-.285-.49-1.432.107-2.985c0 0 .924-.303 3.026 1.156c.877-.25 1.818-.375 2.753-.38c.935.005 1.876.13 2.755.38c2.1-1.459 3.023-1.156 3.023-1.156c.598 1.554.222 2.701.108 2.985c.706.79 1.132 1.796 1.132 3.027c0 4.332-2.573 5.286-5.022 5.565c.394.35.754 1.036.754 2.088v3.095c0 .3.176.652.734.542C19.852 21.484 23 17.258 23 12.277C23 6.048 18.075.999 12 .999" /></svg>
)
},
{
link: "https://discord.com/invite/Y6etn3qKZ3",
icon: () => (
<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.6 18.6 0 0 0-5.487 0a12 12 0 0 0-.617-1.23A.08.08 0 0 0 8.562 3c-1.714.29-3.354.8-4.885 1.491a.1.1 0 0 0-.032.027C.533 9.093-.32 13.555.099 17.961a.08.08 0 0 0 .031.055a20 20 0 0 0 5.993 2.98a.08.08 0 0 0 .084-.026a14 14 0 0 0 1.226-1.963a.074.074 0 0 0-.041-.104a13 13 0 0 1-1.872-.878a.075.075 0 0 1-.008-.125q.19-.14.372-.287a.08.08 0 0 1 .078-.01c3.927 1.764 8.18 1.764 12.061 0a.08.08 0 0 1 .079.009q.18.148.372.288a.075.075 0 0 1-.006.125q-.895.515-1.873.877a.075.075 0 0 0-.041.105c.36.687.772 1.341 1.225 1.962a.08.08 0 0 0 .084.028a20 20 0 0 0 6.002-2.981a.08.08 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>)
},
{
link: "https://www.reddit.com/r/nestri",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135" /></svg>)
},
{
link: "https://x.com/nestriness",
icon: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M8 2H1l8.26 11.015L1.45 22H4.1l6.388-7.349L16 22h7l-8.608-11.478L21.8 2h-2.65l-5.986 6.886zm9 18L5 4h2l12 16z" /></svg>
)
},
]
type Props = {
showBanner?: boolean
}
export const Footer = component$(({ showBanner = true }: Props) => {
return (
<>
{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 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={transition}
client:load
as="div"
class="w-full max-w-xl mx-auto z-[5] flex flex-col border-t-2 dark:border-gray-50/50 border-gray-950/50">
<section class="flex justify-between items-center py-6 border-b-2 dark:border-gray-50/50 border-gray-950/50" >
<div class="flex flex-row gap-1 h-max items-center justify-center">
<svg
width={40} height={40}
viewBox="0 0 12.8778 9.7377253"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg">
<g id="layer1">
<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" />
</g>
</svg>
<h3 class="text-lg font-extrabold font-title">Nestri</h3>
</div>
<p class="text-gray-950 dark:text-gray-50">Your games. Your rules.</p>
</section>
<section class="gap-4 grid grid-cols-2 sm:grid-cols-3 justify-around py-6 items-start" >
<div class="flex flex-col gap-2">
<h2 class="font-title text-sm font-bold" >Product</h2>
<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="/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> */}
<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>
<div class="flex flex-col gap-2">
<h2 class="font-title text-sm font-bold" >Relations</h2>
<div class="text-gray-950/50 dark:text-gray-50/50 flex flex-col gap-2" >
<Link href="/privacy" class="text-base hover:text-gray-950 dark:hover:text-gray-50 hover:underline underline-offset-4 transition-all duration-200" >Privacy Policy</Link>
<Link href="/terms" class="text-base hover:text-gray-950 dark:hover:text-gray-50 hover:underline underline-offset-4 transition-all duration-200" >Terms of Service</Link>
{/**Social Media Icons with Links */}
<div class="flex flex-row gap-3">
{socialMedia.map((item) => (
<Link key={item.link} href={item.link} class=" hover:text-gray-950 dark:hover:text-gray-50" target="_blank" rel="noopener noreferrer">
{item.icon()}
</Link>
))}
</div>
</div>
</div>
</section>
</MotionComponent>
<MotionComponent
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{
...transition,
duration: 0.8,
delay: 0.7
}}
client:load
as="div" class="w-full sm:flex z-[1] hidden pointer-events-none overflow-hidden -mt-[100px] justify-center items-center flex-col" >
<section class='my-0 bottom-0 text-[100%] max-w-[1440px] pointer-events-none w-full flex items-center translate-y-[45%] justify-center relative overflow-hidden px-2 z-10 [&_svg]:w-full [&_svg]:max-w-[1440px] [&_svg]:h-full [&_svg]:opacity-70' >
<svg viewBox="0 0 498.05 70.508" xmlns="http://www.w3.org/2000/svg" height={157} width={695} class="" >
<g stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="currentColor" stroke-width="0.25mm" fill="currentColor" style="stroke:currentColor;stroke-width:0.25mm;fill:currentColor">
<path
fill="url(#paint1)"
pathLength="1"
stroke="url(#paint1)"
d="M 261.23 41.65 L 212.402 41.65 Q 195.313 41.65 195.313 27.002 L 195.313 14.795 A 17.814 17.814 0 0 1 196.311 8.57 Q 199.443 0.146 212.402 0.146 L 283.203 0.146 L 283.203 14.844 L 217.236 14.844 Q 215.337 14.844 214.945 16.383 A 3.67 3.67 0 0 0 214.844 17.285 L 214.844 24.561 Q 214.844 27.002 217.236 27.002 L 266.113 27.002 Q 283.203 27.002 283.203 41.65 L 283.203 53.857 A 17.814 17.814 0 0 1 282.205 60.083 Q 279.073 68.506 266.113 68.506 L 195.313 68.506 L 195.313 53.809 L 261.23 53.809 A 3.515 3.515 0 0 0 262.197 53.688 Q 263.672 53.265 263.672 51.367 L 263.672 44.092 A 3.515 3.515 0 0 0 263.551 43.126 Q 263.128 41.65 261.23 41.65 Z M 185.547 53.906 L 185.547 68.506 L 114.746 68.506 Q 97.656 68.506 97.656 53.857 L 97.656 14.795 A 17.814 17.814 0 0 1 98.655 8.57 Q 101.787 0.146 114.746 0.146 L 168.457 0.146 Q 185.547 0.146 185.547 14.795 L 185.547 31.885 A 17.827 17.827 0 0 1 184.544 38.124 Q 181.621 45.972 170.174 46.538 A 36.906 36.906 0 0 1 168.457 46.582 L 117.188 46.582 L 117.236 51.465 Q 117.236 53.906 119.629 53.955 L 185.547 53.906 Z M 19.531 14.795 L 19.531 68.506 L 0 68.506 L 0 0.146 L 70.801 0.146 Q 87.891 0.146 87.891 14.795 L 87.891 68.506 L 68.359 68.506 L 68.359 17.236 Q 68.359 14.795 65.967 14.795 L 19.531 14.795 Z M 449.219 68.506 L 430.176 46.533 L 400.391 46.533 L 400.391 68.506 L 380.859 68.506 L 380.859 0.146 L 451.66 0.146 A 24.602 24.602 0 0 1 458.423 0.994 Q 466.007 3.166 468.021 10.907 A 25.178 25.178 0 0 1 468.75 17.236 L 468.75 31.885 A 18.217 18.217 0 0 1 467.887 37.73 Q 465.954 43.444 459.698 45.455 A 23.245 23.245 0 0 1 454.492 46.436 L 473.633 68.506 L 449.219 68.506 Z M 292.969 0 L 371.094 0.098 L 371.094 14.795 L 341.846 14.795 L 341.846 68.506 L 322.266 68.506 L 322.217 14.795 L 292.969 14.844 L 292.969 0 Z M 478.516 0.146 L 498.047 0.146 L 498.047 68.506 L 478.516 68.506 L 478.516 0.146 Z M 400.391 14.844 L 400.391 31.885 L 446.826 31.885 Q 448.726 31.885 449.117 30.345 A 3.67 3.67 0 0 0 449.219 29.443 L 449.219 17.285 Q 449.219 14.844 446.826 14.844 L 400.391 14.844 Z M 117.188 31.836 L 163.574 31.934 Q 165.528 31.895 165.918 30.355 A 3.514 3.514 0 0 0 166.016 29.492 L 166.016 17.236 Q 166.016 15.337 164.476 14.945 A 3.67 3.67 0 0 0 163.574 14.844 L 119.629 14.795 Q 117.188 14.795 117.188 17.188 L 117.188 31.836 Z" />
</g>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" id="paint1" x1="317.5" x2="314.007" y1="-51.5" y2="126">
<stop stop-color="white"></stop>
<stop offset="1" stop-opacity="0"></stop>
</linearGradient>
</defs>
</svg>
</section>
</MotionComponent>
</footer>
</>
);
});

View File

@@ -1,34 +1,9 @@
import { SteamLoad, StoreSelect } from "./default";
import { Modal } from "@qwik-ui/headless";
// import { SteamLoad, StoreSelect } from "./default";
import { $, component$, useSignal } from "@builder.io/qwik";
export default component$(() => {
const storeSelect = useSignal(true)
// 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,348 +0,0 @@
import { cn } from "./design";
import Avatar from "./avatar"
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$(({ 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 onDialogOpen = $((open: boolean) => {
if (open) {
disablePageScroll()
} else {
enablePageScroll()
}
})
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>
</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 text-[#6f6f6f] dark:text-[#a0a0a0]">
<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="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" >
{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">
<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="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>
<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"><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>
</button>
</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
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 "
>
<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="M13 22H6.59c-1.545 0-2.774-.752-3.877-1.803c-2.26-2.153 1.45-3.873 2.865-4.715a10.67 10.67 0 0 1 7.922-1.187m3-7.795a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0M16 22l3-3m0 0l3-3m-3 3l-3-3m3 3l3 3" color="currentColor" /></svg>
<span class="group-hover:hidden">Leave Team</span>
<span class="hidden group-hover:block">Hold to leave</span>
</span>
</Dropdown.Item>
</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

@@ -0,0 +1,135 @@
import { cn } from "../design";
import Avatar from "../avatar";
import type Nestri from "@nestri/sdk"
import { Tooltip } from '@qwik-ui/headless';
import { useNavigate } from "@builder.io/qwik-city";
import { $, component$, useOnDocument, useSignal, type QRL } from "@builder.io/qwik";
type Props = {
getActiveUsers$: QRL<() => Promise<Nestri.Users.UserListResponse.Data[] | undefined>>
getSession$: QRL<(profileID: string) => Promise<Nestri.Users.UserSessionResponse | undefined>>
}
const skeletonCrew = new Array(3).fill(0)
export const HomeFriendsSection = component$(({ getActiveUsers$, getSession$ }: Props) => {
const activeUsers = useSignal<Nestri.Users.UserListResponse.Data[] | undefined>()
const nav = useNavigate()
useOnDocument("load", $(async () => {
const sessionUserData = sessionStorage.getItem("active_user_data")
if (!sessionUserData) {
const users = await getActiveUsers$()
sessionStorage.setItem("active_user_data", JSON.stringify(users))
activeUsers.value = users
} else {
activeUsers.value = JSON.parse(sessionUserData)
}
}))
const onWatch = $(async (profileID: string) => {
const session = await getSession$(profileID)
await nav(`/play/${session?.data.id}`)
})
return (
<div class="gap-2 w-full flex-col flex">
<hr class="border-none h-[1.5px] dark:bg-gray-700 bg-gray-300 w-full" />
<div class="flex flex-col justify-center py-2 px-3 items-start w-full ">
<div class="text-gray-600/70 dark:text-gray-400/70 leading-none flex justify-between items-center w-full py-1">
<span class="text-xl text-gray-700 dark:text-gray-300 leading-none font-bold font-title flex gap-2 items-center pb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 20 20"><path fill="currentColor" d="M2.049 9.112a8.001 8.001 0 1 1 9.718 8.692a1.5 1.5 0 0 0-.206-1.865l-.01-.01q.244-.355.47-.837a9.3 9.3 0 0 0 .56-1.592H9.744q.17-.478.229-1h2.82A15 15 0 0 0 13 10c0-.883-.073-1.725-.206-2.5H7.206l-.05.315a4.5 4.5 0 0 0-.971-.263l.008-.052H3.46q-.112.291-.198.595c-.462.265-.873.61-1.213 1.017m9.973-4.204C11.407 3.59 10.657 3 10 3s-1.407.59-2.022 1.908A9.3 9.3 0 0 0 7.42 6.5h5.162a9.3 9.3 0 0 0-.56-1.592M6.389 6.5c.176-.743.407-1.422.683-2.015c.186-.399.401-.773.642-1.103A7.02 7.02 0 0 0 3.936 6.5zm9.675 7H13.61a10.5 10.5 0 0 1-.683 2.015a6.6 6.6 0 0 1-.642 1.103a7.02 7.02 0 0 0 3.778-3.118m-2.257-1h2.733c.297-.776.46-1.62.46-2.5s-.163-1.724-.46-2.5h-2.733c.126.788.193 1.63.193 2.5s-.067 1.712-.193 2.5m2.257-6a7.02 7.02 0 0 0-3.778-3.118c.241.33.456.704.642 1.103c.276.593.507 1.272.683 2.015zm-7.76 7.596a3.5 3.5 0 1 0-.707.707l2.55 2.55a.5.5 0 0 0 .707-.707zM8 12a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0" /></svg>
Find people to play with
</span>
</div>
<ul class="list-none ml-4 relative w-[calc(100%-1rem)]">
{activeUsers.value ? (
activeUsers.value.slice(0, 3).map((user, key) => (
<div key={`user-${key}`} >
<div class="gap-3.5 hover:bg-gray-200 dark:hover:bg-gray-800 text-left outline-none group rounded-lg px-3 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] flex items-center w-full">
<div class="relative [&>svg]:size-[80px] [&>img]:size-[80px] min-w-[80px]">
{user.avatarUrl ?
(<img height={52} width={52} draggable={false} class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] ring-2 ring-gray-200 dark:ring-gray-800 select-none rounded-full aspect-square w-[80px]" src={user.avatarUrl} alt={user.username} />) :
(<Avatar name={`${user.username}#${user.discriminator}`} />)}
</div>
<div class={cn("w-full h-[100px] overflow-hidden pr-2 border-b-2 border-gray-400/70 dark:border-gray-700/70 flex gap-2 items-center", key == 2 && "border-none")}>
<div class="flex-col">
<span class="font-medium tracking-tighter text-gray-700 dark:text-gray-300 max-w-full text-lg truncate leading-none flex [&>svg]:size-5 [&>svg]:dark:text-[#12ECFA] ">
{`${user.username}`}&nbsp;<p class="hidden group-hover:block text-gray-600/70 dark:text-gray-400/70 transition-all duration-200 ease-in">{` #${user.discriminator}`}</p>
{/* &nbsp;{user.status && (user.status == "active" && (<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M10.277 16.515c.005-.11.186-.154.24-.058c.254.45.686 1.111 1.176 1.412s1.276.386 1.792.408c.11.005.153.186.057.24c-.45.254-1.11.686-1.411 1.176s-.386 1.276-.408 1.792c-.005.11-.187.153-.24.057c-.254-.45-.686-1.11-1.177-1.411c-.49-.301-1.276-.386-1.791-.408c-.11-.005-.154-.187-.058-.24c.45-.254 1.111-.686 1.412-1.177c.3-.49.386-1.276.408-1.791"/><path fill="currentColor" d="M18.492 15.515c-.009-.11-.2-.156-.258-.062c-.172.283-.42.623-.697.793s-.692.236-1.022.262c-.11.008-.156.2-.062.257c.282.172.623.42.793.697s.236.693.262 1.023c.008.11.2.155.257.061c.172-.282.42-.623.697-.792s.693-.237 1.023-.262c.11-.009.155-.2.061-.258c-.282-.172-.623-.42-.792-.697s-.237-.692-.262-1.022" opacity=".5"/><path fill="currentColor" d="m14.703 4.002l-.242-.306c-.937-1.183-1.405-1.775-1.95-1.688c-.544.088-.805.796-1.326 2.213l-.135.366c-.148.403-.222.604-.364.752s-.336.225-.724.38l-.353.141l-.247.1c-1.2.48-1.804.753-1.882 1.283c-.082.565.49 1.049 1.634 2.016l.296.25c.326.275.488.413.581.6c.094.187.107.403.133.835l.024.393c.094 1.52.14 2.28.635 2.542c.494.262 1.108-.147 2.336-.966l.318-.212c.349-.233.523-.35.723-.381s.401.024.806.136l.367.102c1.423.394 2.134.591 2.521.188c.388-.403.195-1.14-.19-2.613l-.1-.381c-.109-.419-.164-.628-.134-.835s.142-.389.366-.752l.203-.33c.785-1.276 1.178-1.914.924-2.426c-.255-.51-.988-.557-2.454-.648l-.38-.024c-.416-.026-.624-.039-.805-.135s-.314-.264-.58-.6"/><path fill="currentColor" d="M8.835 13.326C6.698 14.37 4.919 16.024 4.248 18c-.752-4.707.292-7.747 1.965-9.637c.144.295.332.539.5.73c.35.396.852.82 1.362 1.251l.367.31l.17.145c.005.064.01.14.015.237l.03.485c.04.655.08 1.294.178 1.805" opacity=".5"/></svg>))} */}
</span>
<div class="flex items-center gap-2 w-full cursor-pointer px-1 rounded-md">
<div class={cn("font-normal capitalize w-full text-gray-600/70 dark:text-gray-400/70 truncate flex gap-1 items-center", (user.status && user.status == "active") && "dark:text-[#50e3c2]")}>
<div class={cn("size-3 rounded-full block", user.status ? (user.status == "active" ? "bg-[#50e3c2]" : user.status == "idle" ? "bg-[#ff990a]" : "hidden") : "hidden")} />
{user.status ? (user.status == "active" ? "Playing Steam" : user.status) : "Offline"}
</div>
</div>
</div>
<div class="ml-auto relative flex gap-2 justify-center h-full items-center">
{user.status && (user.status == "active" &&
(
<Tooltip.Root gutter={12} flip={false} placement="top" >
<Tooltip.Trigger onClick$={async () => { await onWatch(user.id) }} type="button" class="bg-gray-200 group-hover:bg-gray-300 dark:group-hover:bg-gray-700 transition-all duration-200 ease-in text-gray-600 dark:text-gray-400 dark:bg-gray-800 [&>svg]:size-5 p-2 rounded-full" >
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M4.75 5.5c.427 0 .791.163 1.075.45c-.145-.778-.37-1.415-.64-1.894C4.72 3.236 4.216 3 3.75 3s-.97.237-1.434 1.056C1.835 4.906 1.5 6.25 1.5 8s.335 3.094.816 3.944c.463.82.967 1.056 1.434 1.056s.97-.237 1.434-1.056c.272-.48.496-1.116.64-1.895a1.47 1.47 0 0 1-1.074.451C3.674 10.5 3 9.47 3 8s.674-2.5 1.75-2.5M7.5 8c0 3.822-1.445 6.5-3.75 6.5S0 11.822 0 8s1.445-6.5 3.75-6.5S7.5 4.178 7.5 8m6.825 2.05c-.145.778-.37 1.415-.64 1.894c-.464.82-.968 1.056-1.435 1.056s-.97-.237-1.434-1.056C10.335 11.094 10 9.75 10 8s.335-3.094.816-3.944C11.279 3.236 11.783 3 12.25 3s.97.237 1.434 1.056c.272.48.496 1.116.64 1.895A1.47 1.47 0 0 0 13.25 5.5c-1.076 0-1.75 1.03-1.75 2.5s.674 2.5 1.75 2.5a1.47 1.47 0 0 0 1.075-.45M16 8c0 3.822-1.445 6.5-3.75 6.5S8.5 11.822 8.5 8s1.445-6.5 3.75-6.5S16 4.178 16 8" clip-rule="evenodd" /></svg>
</Tooltip.Trigger>
<Tooltip.Panel class="tooltip capitalize text-sm relative rounded-md dark:bg-gray-700 bg-gray-300 py-2 px-3 text-gray-700 dark:text-gray-300" aria-label="More options">
Watch stream
<div class="-bottom-2 left-1/2 -translate-x-1/2 absolute dark:text-gray-700 text-gray-300" >
<svg width="15" height="10" viewBox="0 0 30 10" preserveAspectRatio="none" fill="currentColor"><polygon points="0,0 30,0 15,10"></polygon></svg>
</div>
</Tooltip.Panel>
</Tooltip.Root>
))}
<Tooltip.Root gutter={12} flip={false} placement="top" >
<Tooltip.Trigger disabled type="button" class="bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed group-hover:bg-gray-300 dark:group-hover:bg-gray-700 transition-all duration-200 ease-in text-gray-600 dark:text-gray-400 dark:bg-gray-800 [&>svg]:size-5 p-2 rounded-full" >
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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>
</Tooltip.Trigger>
<Tooltip.Panel class="tooltip capitalize text-sm relative rounded-md dark:bg-gray-700 bg-gray-300 py-2 px-3 text-gray-700 dark:text-gray-300" aria-label="More options">
Invite
<div class="-bottom-2 left-1/2 -translate-x-1/2 absolute dark:text-gray-700 text-gray-300" >
<svg width="15" height="10" viewBox="0 0 30 10" preserveAspectRatio="none" fill="currentColor"><polygon points="0,0 30,0 15,10"></polygon></svg>
</div>
</Tooltip.Panel>
</Tooltip.Root>
<Tooltip.Root gutter={12} flip={false} placement="top" >
<Tooltip.Trigger disabled type="button" class="bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed group-hover:bg-gray-300 dark:group-hover:bg-gray-700 transition-all duration-200 ease-in text-gray-600 dark:text-gray-400 dark:bg-gray-800 [&>svg]:size-5 p-2 rounded-full" >
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2m0 2c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0 6c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2" /></svg>
</Tooltip.Trigger>
<Tooltip.Panel class="tooltip capitalize text-sm relative rounded-md dark:bg-gray-700 bg-gray-300 py-2 px-3 text-gray-700 dark:text-gray-300" aria-label="More options">
more
<div class="-bottom-2 left-1/2 -translate-x-1/2 absolute dark:text-gray-700 text-gray-300" >
<svg width="15" height="10" viewBox="0 0 30 10" preserveAspectRatio="none" fill="currentColor"><polygon points="0,0 30,0 15,10"></polygon></svg>
</div>
</Tooltip.Panel>
</Tooltip.Root>
</div>
</div>
</div>
</div>
))
) :
(
skeletonCrew.map((_, key) => (
<div key={`skeleton-friend-${key}`} >
<div class="gap-3.5 text-left animate-pulse outline-none group rounded-lg px-3 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] flex items-center w-full">
<div class="relative w-max">
<div class="size-20 rounded-full bg-gray-200 dark:bg-gray-800" />
</div>
<div class={cn("w-full h-[100px] overflow-hidden pr-2 border-b-2 border-gray-400/70 dark:border-gray-700/70 flex gap-2 items-center", key == 2 && "border-none")}>
<div class="flex-col w-[80%] gap-2 flex">
<span class="font-medium tracking-tighter bg-gray-200 dark:bg-gray-800 rounded-md h-6 w-2/3 max-w-full text-lg font-title truncate leading-none block" />
<div class="flex items-center gap-2 w-full h-6 bg-gray-200 dark:bg-gray-800 rounded-md" />
</div>
<div class="bg-gray-200 dark:bg-gray-800 h-7 w-16 ml-auto rounded-md" />
</div>
</div>
</div>
))
)
}
<div class="[border:1px_dashed_theme(colors.gray.300)] dark:[border:1px_dashed_theme(colors.gray.800)] [mask-image:linear-gradient(rgb(0,0,0)_0%,_rgb(0,0,0)_calc(100%-120px),_transparent_100%)] bottom-0 top-0 -left-[0.4625rem] absolute" />
</ul>
</div>
</div>
)
})

View File

@@ -0,0 +1,102 @@
import type Nestri from "@nestri/sdk";
import { useNavigate } from "@builder.io/qwik-city";
import { $, component$, useOnDocument, useSignal, type QRL } from "@builder.io/qwik";
type Props = {
getUserSubscription$: QRL<() => Promise<"Free" | "Pro" | undefined>>
createSession$: QRL<() => Promise<Nestri.Tasks.TaskSessionResponse.Data | undefined>>
}
const skeletonGames = new Array(6).fill(0)
export const HomeGamesSection = component$(({ getUserSubscription$ }: Props) => { //createSession$
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const nav = useNavigate()
const creatingSession = useSignal(false)
const userSubscription = useSignal<"Free" | "Pro" | undefined>()
useOnDocument("load", $(async () => {
const userSub = sessionStorage.getItem("subscription_data")
if (userSub) {
userSubscription.value = JSON.parse(userSub)
} else {
const subscription = await getUserSubscription$()
sessionStorage.setItem("subscription_data", JSON.stringify(subscription))
userSubscription.value = subscription
}
}))
const onClick = $(async () => {
console.log("clicked")
// creatingSession.value = true
// const sessionID = await createSession$()
// if (sessionID) {
// creatingSession.value = false
// await nav(`/play/${sessionID.id}`)
// }
});
return (
<div class="gap-2 w-full flex-col flex">
<hr class="border-none h-[1.5px] dark:bg-gray-700 bg-gray-300 w-full" />
<div class="text-gray-600/70 dark:text-gray-400/70 text-sm leading-none flex justify-start py-2 px-3 items-end">
<span class="text-xl text-gray-700 dark:text-gray-300 leading-none font-bold font-title flex gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 22c-.818 0-1.6-.33-3.163-.99C4.946 19.366 3 18.543 3 17.16V7m9 15c.818 0 1.6-.33 3.163-.99C19.054 19.366 21 18.543 21 17.16V7m-9 15V11.355M8.326 9.691L5.405 8.278C3.802 7.502 3 7.114 3 6.5s.802-1.002 2.405-1.778l2.92-1.413C10.13 2.436 11.03 2 12 2s1.871.436 3.674 1.309l2.921 1.413C20.198 5.498 21 5.886 21 6.5s-.802 1.002-2.405 1.778l-2.92 1.413C13.87 10.564 12.97 11 12 11s-1.871-.436-3.674-1.309M6 12l2 1m9-9L7 9" color="currentColor" /></svg>
Your Games
</span>
{/* {userSubscription.value ? (
<button disabled={userSubscription.value === "Free"} class="disabled:opacity-50 disabled:cursor-not-allowed ml-auto flex gap-1 items-center cursor-pointer [&:not(:disabled)]:hover:text-gray-800 dark:[&:not(:disabled)]:hover:text-gray-200 transition-all duration-200 outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 256 256"><path fill="currentColor" d="M248 128a87.34 87.34 0 0 1-17.6 52.81a8 8 0 1 1-12.8-9.62A71.34 71.34 0 0 0 232 128a72 72 0 0 0-144 0a8 8 0 0 1-16 0a88 88 0 0 1 3.29-23.88C74.2 104 73.1 104 72 104a48 48 0 0 0 0 96h24a8 8 0 0 1 0 16H72a64 64 0 1 1 9.29-127.32A88 88 0 0 1 248 128m-69.66 42.34L160 188.69V128a8 8 0 0 0-16 0v60.69l-18.34-18.35a8 8 0 0 0-11.32 11.32l32 32a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" /></svg>
<span>Install a game</span>
</button>
) : (
<div class="ml-auto h-4 w-28 rounded-md bg-gray-200 dark:gray-800 animate-pulse" />
)} */}
</div>
<ul class="relative py-3 w-full list-none after:pointer-events-none after:select-none after:w-full after:h-[120px] after:fixed after:z-10 after:backdrop-blur-[1px] after:bg-gradient-to-b after:from-transparent after:to-gray-200 dark:after:to-gray-800 after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.200)_25%,transparent)] dark:after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.800)_25%,transparent)] after:left-0 after:-bottom-[1px]">
{userSubscription.value ? (
<div class="flex flex-col items-center justify-center gap-6 px-6 py-20 w-full" >
<div class="relative flex items-center justify-center overflow-hidden rounded-[22px] p-[2px] before:absolute before:left-[-50%] before:top-[-50%] before:z-[-2] before:h-[200%] before:w-[200%] before:animate-[bgRotate_1.15s_linear_infinite] before:bg-[conic-gradient(from_0deg,transparent_0%,#ff4f01_10%,#ff4f01_25%,transparent_35%)] before:content-[''] after:absolute after:inset-[2px] after:z-[-1] after:content-['']" >
<div class="flex items-center justify-center rounded-[20px] bg-gray-200 dark:bg-gray-800 p-1">
<div class="flex items-center justify-center rounded-2xl bg-[#F5F5F5] p-1 dark:bg-[#171717]">
<div class="flex h-[64px] w-[64px] items-center justify-center rounded-xl bg-gray-100 dark:bg-gray-900">
<svg xmlns="http://www.w3.org/2000/svg" width="32" class="h-8 w-8 shrink-0 dark:text-gray-700 text-gray-300" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11.968 2C6.767 2 2.4 6.045 2.048 11.181l5.329 2.216c.45-.322.995-.45 1.573-.45h.128l2.344-3.5v-.031a3.74 3.74 0 0 1 3.756-3.756c2.087 0 3.788 1.67 3.788 3.756a3.74 3.74 0 0 1-3.756 3.756h-.096l-3.403 2.44v.128a2.863 2.863 0 0 1-2.857 2.857c-1.349 0-2.536-.995-2.761-2.247l-3.724-1.637C3.557 18.886 7.44 22 11.968 22c5.49-.032 9.984-4.494 9.984-10.016S17.457 2 11.968 2" /><path fill="currentColor" d="m8.276 17.152l-1.22-.481c.225.45.578.867 1.092 1.027c1.027.45 2.311-.032 2.76-1.123a2.07 2.07 0 0 0 0-1.638a2.26 2.26 0 0 0-1.123-1.187c-.514-.225-1.027-.193-1.54-.033l1.251.546c.77.353 1.188 1.252.867 2.023c-.353.802-1.252 1.155-2.087.866m9.502-7.736c0-1.349-1.124-2.536-2.536-2.536c-1.349 0-2.536 1.123-2.536 2.536c0 1.412 1.188 2.536 2.536 2.536s2.536-1.156 2.536-2.536m-4.366 0c0-1.027.867-1.862 1.862-1.862c1.027 0 1.862.867 1.862 1.862c0 1.027-.867 1.862-1.862 1.862c-1.027.032-1.862-.835-1.862-1.862" /></svg>
</div>
</div>
</div>
</div>
<div class="flex flex-col items-center justify-center gap-1">
<span class="select-none text-center text-gray-700 dark:text-gray-300 font-title text-xl font-semibold sm:font-medium">Waiting for your first game install</span>
<p class="text-center text-base font-medium text-gray-600 dark:text-gray-400 sm:font-regular">Once you have installed a game on your machine, it should appear here</p>
</div>
<button
onClick$={onClick}
// disabled={userSubscription.value === "Free"}
disabled
class="flex gap-2 h-[48px] disabled:cursor-not-allowed disabled:opacity-50 max-w-[360px] w-full select-none items-center justify-center rounded-full bg-primary-500 text-base font-semibold text-white transition-all duration-200 ease-out [&:not(:disabled)]:hover:ring-2 [&:not(:disabled)]:hover:ring-gray-600 dark:[&:not(:disabled)]:hover:ring-gray-400 [&:not(:disabled)]:focus:scale-95 [&:not(:disabled)]:active:scale-95 sm:font-medium">
{creatingSession.value &&
<div style={{ "--spinner-color": "#FFF" }} data-component="spinner">
<div>
{new Array(12).fill(0).map((i, k) => (
<div key={k} />
))}
</div>
</div>
}
<span> {creatingSession.value ? "Launching Steam" : "Launch Steam"}</span>
</button>
</div>
) : (
<div class="grid sm:grid-cols-3 grid-cols-2 gap-2 gap-y-3 w-full animate-pulse" >
{skeletonGames.map((_, key) => (
<div key={`skeleton-game-${key}`} class="w-full gap-2 flex flex-col" >
<div class="bg-gray-200 dark:bg-gray-800 w-full aspect-square rounded-2xl" />
</div>
))}
</div>
)}
</ul >
</div >
)
})

View File

@@ -0,0 +1,131 @@
/* eslint-disable qwik/jsx-img */
import { cn } from "../design";
import { MotionComponent } from "../react";
import { $, component$, useOnDocument, useSignal, type QRL } from "@builder.io/qwik";
import { Link } from "@builder.io/qwik-city";
type Props = {
getUserSubscription$: QRL<() => Promise<"Free" | "Pro" | undefined>>
}
export const HomeMachineSection = component$(({ getUserSubscription$ }: Props) => {
const isHovered = useSignal(false)
const userSubscription = useSignal<"Free" | "Pro" | undefined>()
useOnDocument("load", $(async () => {
const userSub = sessionStorage.getItem("subscription_data")
if (userSub) {
userSubscription.value = JSON.parse(userSub)
} else {
const subscription = await getUserSubscription$()
sessionStorage.setItem("subscription_data", JSON.stringify(subscription))
userSubscription.value = subscription
}
}))
return (
<div class="flex flex-col gap-6 w-full py-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{userSubscription.value ? (
<>
{userSubscription.value == "Pro" ? (
<div class="w-full animate-fade-in opacity-0 transition-all duration-200 ease-in" >
<Link href="/machine" 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 truncate group">
<div class="flex items-center justify-between group gap-2">
<div class="flex items-center w-auto gap-2 overflow-hidden">
<div class="flex items-center gap-2">
<div class="w-auto select-none text-nowrap font-medium transition-colors duration-200 ease-out group-hover:text-black text-black/80 dark:group-hover:text-white dark:text-white/80">Steam Machine</div>
</div>
</div>
{/* <div class="select-none flex gap-1 items-center overflow-hidden rounded-md px-2 py-1 text-xs font-medium transition-colors duration-200 ease-out" > */}
<div class={cn("select-none overflow-hidden flex gap-1 uppercase items-center rounded-md px-2 py-1 text-xs font-medium transition-colors duration-200 ease-out", userSubscription.value === "Pro" ? "bg-[#0CCE6B]/30 text-[#0CCE6B] group-hover:bg-[#0CCE6B]/40" : " bg-[#EE0048]/30 text-[#EE0048] hover:bg-[#EE0048]/40")}>
<div class={cn("size-2 rounded-full", userSubscription.value == "Pro" ? "bg-[#0CCE6B]" : "bg-[#EE0048]")} />
<span>{userSubscription.value == "Pro" ? "Online" : "Error"}</span>
</div>
</div>
</div>
<div
style={{
"--cutout-avatar-percentage-visible": 0.2,
"--head-margin-percentage": 0.1,
"--size": "3rem"
}}
class="relative h-full flex w-[20%] 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>
</Link>
</div>
) : (
<div class="w-full animate-fade-in opacity-0 transition-all duration-200 ease-in relative">
<MotionComponent
client:load
as="span"
initial={{ x: 80, y: 0, rotate: 0 }}
animate={{
x: isHovered.value ? 5 : 80,
y: isHovered.value ? -20 : 0,
rotate: isHovered.value ? 720 : 0
}}
transition={{
type: "spring",
stiffness: 200,
damping: 15,
}}
class="absolute text-[#99CCFF] flex items-center justify-center z-[1]">
<svg height="16" class="size-10" stroke-linejoin="round" viewBox="0 0 16 16" width="16" color="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49999 0H6.49999L6.22628 1.45975C6.1916 1.64472 6.05544 1.79299 5.87755 1.85441C5.6298 1.93996 5.38883 2.04007 5.15568 2.15371C4.98644 2.2362 4.78522 2.22767 4.62984 2.12136L3.40379 1.28249L1.28247 3.40381L2.12135 4.62986C2.22766 4.78524 2.23619 4.98646 2.1537 5.15569C2.04005 5.38885 1.93995 5.62981 1.8544 5.87756C1.79297 6.05545 1.6447 6.19162 1.45973 6.2263L0 6.5V9.5L1.45973 9.7737C1.6447 9.80838 1.79297 9.94455 1.8544 10.1224C1.93995 10.3702 2.04006 10.6112 2.1537 10.8443C2.23619 11.0136 2.22767 11.2148 2.12136 11.3702L1.28249 12.5962L3.40381 14.7175L4.62985 13.8786C4.78523 13.7723 4.98645 13.7638 5.15569 13.8463C5.38884 13.9599 5.6298 14.06 5.87755 14.1456C6.05544 14.207 6.1916 14.3553 6.22628 14.5403L6.49999 16H9.49999L9.77369 14.5403C9.80837 14.3553 9.94454 14.207 10.1224 14.1456C10.3702 14.06 10.6111 13.9599 10.8443 13.8463C11.0135 13.7638 11.2147 13.7723 11.3701 13.8786L12.5962 14.7175L14.7175 12.5962L13.8786 11.3701C13.7723 11.2148 13.7638 11.0135 13.8463 10.8443C13.9599 10.6112 14.06 10.3702 14.1456 10.1224C14.207 9.94455 14.3553 9.80839 14.5402 9.7737L16 9.5V6.5L14.5402 6.2263C14.3553 6.19161 14.207 6.05545 14.1456 5.87756C14.06 5.62981 13.9599 5.38885 13.8463 5.1557C13.7638 4.98647 13.7723 4.78525 13.8786 4.62987L14.7175 3.40381L12.5962 1.28249L11.3701 2.12137C11.2148 2.22768 11.0135 2.2362 10.8443 2.15371C10.6111 2.04007 10.3702 1.93996 10.1224 1.85441C9.94454 1.79299 9.80837 1.64472 9.77369 1.45974L9.49999 0ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="currentColor"></path>
</svg>
</MotionComponent>
<MotionComponent
as="span"
client:load
initial={{ x: 80, y: 0, rotate: 0 }}
animate={{
x: isHovered.value ? 100 : 20,
y: isHovered.value ? 35 : 0,
rotate: isHovered.value ? 720 : 0
}}
transition={{
type: "spring",
stiffness: 200,
damping: 15,
}}
class="absolute text-[#99CCFF] flex items-center justify-center z-[1]">
<svg height="16" class="size-10" stroke-linejoin="round" viewBox="0 0 16 16" width="16" color="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49999 0H6.49999L6.22628 1.45975C6.1916 1.64472 6.05544 1.79299 5.87755 1.85441C5.6298 1.93996 5.38883 2.04007 5.15568 2.15371C4.98644 2.2362 4.78522 2.22767 4.62984 2.12136L3.40379 1.28249L1.28247 3.40381L2.12135 4.62986C2.22766 4.78524 2.23619 4.98646 2.1537 5.15569C2.04005 5.38885 1.93995 5.62981 1.8544 5.87756C1.79297 6.05545 1.6447 6.19162 1.45973 6.2263L0 6.5V9.5L1.45973 9.7737C1.6447 9.80838 1.79297 9.94455 1.8544 10.1224C1.93995 10.3702 2.04006 10.6112 2.1537 10.8443C2.23619 11.0136 2.22767 11.2148 2.12136 11.3702L1.28249 12.5962L3.40381 14.7175L4.62985 13.8786C4.78523 13.7723 4.98645 13.7638 5.15569 13.8463C5.38884 13.9599 5.6298 14.06 5.87755 14.1456C6.05544 14.207 6.1916 14.3553 6.22628 14.5403L6.49999 16H9.49999L9.77369 14.5403C9.80837 14.3553 9.94454 14.207 10.1224 14.1456C10.3702 14.06 10.6111 13.9599 10.8443 13.8463C11.0135 13.7638 11.2147 13.7723 11.3701 13.8786L12.5962 14.7175L14.7175 12.5962L13.8786 11.3701C13.7723 11.2148 13.7638 11.0135 13.8463 10.8443C13.9599 10.6112 14.06 10.3702 14.1456 10.1224C14.207 9.94455 14.3553 9.80839 14.5402 9.7737L16 9.5V6.5L14.5402 6.2263C14.3553 6.19161 14.207 6.05545 14.1456 5.87756C14.06 5.62981 13.9599 5.38885 13.8463 5.1557C13.7638 4.98647 13.7723 4.78525 13.8786 4.62987L14.7175 3.40381L12.5962 1.28249L11.3701 2.12137C11.2148 2.22768 11.0135 2.2362 10.8443 2.15371C10.6111 2.04007 10.3702 1.93996 10.1224 1.85441C9.94454 1.79299 9.80837 1.64472 9.77369 1.45974L9.49999 0ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="currentColor"></path>
</svg>
</MotionComponent>
<div class="hover:border-[#99CCFF]/70 border-gray-300 dark:border-gray-700 bg-white dark:bg-black z-[5] relative w-full transition-all duration-300 border-[2px] h-14 rounded-xl px-4 gap-2 flex items-center justify-between overflow-hidden outline-none disabled:opacity-50">
<span class="p-2 pl-0 text-gray-800/70 dark:text-gray-200/70 leading-none shrink truncate flex text-start items-center gap-2">
<a
onMouseEnter$={() => isHovered.value = true}
onMouseLeave$={() => isHovered.value = false}
href="https://polar.sh/nestri" class="dark:text-white text-black border-b border-[#99CCFF] py-0.5">Upgrade to Pro</a>&nbsp;to get a machine
</span>
</div>
</div>
)}
<button class="w-full animate-fade-in opacity-0 transition-all duration-200 ease-in">
<div class="border-gray-400/70 w-full dark:border-gray-700/70 transition-all border-dashed duration-200 border-[2px] h-14 rounded-xl px-4 gap-2 flex items-center justify-between overflow-hidden outline-none disabled:opacity-50">
<span class="p-2 pl-0 text-gray-600/70 dark:text-gray-400/70 leading-none shrink truncate flex text-start 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 your machine
<div class="select-none text-[#ff990a] bg-[#ff990a]/30 h-max uppercase overflow-hidden rounded-md px-2 py-1 text-xs transition-colors duration-200 ease-out font-semibold font-title">
<span>Soon</span>
</div>
</span>
</div>
</button>
</>
) :
new Array(2).fill(0).map((_, key) => (
<div class="w-full animate-pulse" key={`skeleton-machine-${key}`}>
<div class="rounded-xl bg-gray-200 dark:bg-gray-800 h-14 w-full" />
</div>
))
}
</div>
</div >
)
})

View File

@@ -0,0 +1,393 @@
import { cn } from "../design";
import Avatar from "../avatar"
import type Nestri from "@nestri/sdk"
import { MotionComponent } from "../react";
import { Dropdown, Modal } from '@qwik-ui/headless';
import { useLocation, useNavigate } from "@builder.io/qwik-city";
import { disablePageScroll, enablePageScroll } from '@fluejs/noscroll';
import { $, component$, type QRL, useOnDocument, useSignal } from "@builder.io/qwik";
type Props = {
getUserProfile$: QRL<() => Promise<Nestri.Users.UserRetrieveResponse.Data | undefined>>
}
export const HomeNavBar = component$(({ getUserProfile$ }: Props) => {
const nav = useNavigate()
const inviteEmail = useSignal('');
const location = useLocation().url
const inviteName = useSignal('');
const isHolding = useSignal(false);
const newTeamName = useSignal('');
const isNewTeam = useSignal(false);
const hasScrolled = useSignal(false);
const isNewMember = useSignal(false);
const showInviteSuccess = useSignal(false);
const userProfile = useSignal<Nestri.Users.UserRetrieveResponse.Data | undefined>()
const onDialogOpen = $((open: boolean) => {
if (open) {
disablePageScroll()
} else {
enablePageScroll()
}
})
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 handleLogout = $(async() => {
await nav(`/auth/logout`)
});
const handleLogoutAnimationComplete = $(() => {
if (isHolding.value) {
console.log("fucking hell")
// isDeleting.value = true;
// Reset the holding state
isHolding.value = false;
handleLogout();
}
});
useOnDocument("load", $(async () => {
const currentProfile = await getUserProfile$()
userProfile.value = currentProfile
}))
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>
</div>
<div class="relative z-[5] 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" />
{userProfile.value ? (
<Dropdown.Root class="animate-fade-in opacity-0 transition-all duration-200 ease-in" 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={`${userProfile.value.username}'s Games`} />
<span class="truncate shrink max-w-[20ch]">{`${userProfile.value.username}'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>
<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 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">
<Dropdown.RadioItem
value={`${userProfile.value.username}'s Games`}
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 text-[#6f6f6f] dark:text-[#a0a0a0]">
<Avatar class="flex-shrink-0 rounded-full" name={`${userProfile.value.username}'s Games`} />
{`${userProfile.value.username}'s Games`}
</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={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", "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="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", "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={true}
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", "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"
}}
/>
<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 class="h-6 w-40 rounded-md bg-gray-200 dark:bg-gray-800 animate-pulse" />)}
{location.pathname == "/machine" &&
(<>
<hr class="dark:bg-gray-700/70 bg-gray-400/70 w-0.5 rounded-md mx-3 rotate-[16deg] h-7 border-none" />
{userProfile.value ? (
<div 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 ">
<span class="truncate shrink max-w-[20ch]">{`Steam Machine`}</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>
</div>
) : (<div class="h-6 w-40 rounded-md bg-gray-200 dark:bg-gray-800 animate-pulse" />)}
</>
)
}
</div>
</div>
<div class="gap-4 flex flex-row justify-center h-full items-center">
{userProfile.value ?
(<Dropdown.Root class="animate-fade-in opacity-0 transition-all duration-200 ease-in" 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" >
{userProfile.value.avatarUrl ? (<img src={userProfile.value.avatarUrl} height={20} width={20} class="size-6 rounded-full" alt="Avatar" />) : (<Avatar name={`${userProfile.value.username}#${userProfile.value.discriminator}`} />)}
<span class="truncate shrink max-w-[20ch] sm:flex hidden">{`${userProfile.value.username}#${userProfile.value.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">
<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="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>
{/* <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"
}}
onAnimationComplete$={handleLogoutAnimationComplete}
/>
<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>
<span class="group-hover:hidden">Log out</span>
<span class="hidden group-hover:block">Hold to logout</span>
</span>
</button> */}
<button
onPointerDown$={handlePointerDown}
onPointerUp$={handlePointerUp}
onPointerLeave$={handlePointerUp}
onKeyDown$={(e) => e.key === "Enter" && handlePointerDown()}
onKeyUp$={(e) => e.key === "Enter" && handlePointerUp()}
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")}>
<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$={handleLogoutAnimationComplete}
/>
<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>
<span class="group-hover:hidden">Log out</span>
<span class="hidden group-hover:block">Hold to logout</span>
</span>
</button>
</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
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 "
>
<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="M13 22H6.59c-1.545 0-2.774-.752-3.877-1.803c-2.26-2.153 1.45-3.873 2.865-4.715a10.67 10.67 0 0 1 7.922-1.187m3-7.795a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0M16 22l3-3m0 0l3-3m-3 3l-3-3m3 3l3 3" color="currentColor" /></svg>
<span class="group-hover:hidden">Leave Team</span>
<span class="hidden group-hover:block">Hold to leave</span>
</span>
</Dropdown.Item>
</Dropdown.Group> */}
</Dropdown.Popover>
</Dropdown.Root>) :
(<div class="flex gap-2 justify-center items-center animate-pulse " >
<div class="h-6 w-20 rounded-md bg-gray-200 dark:bg-gray-800 right-4" />
<div class="size-8 rounded-full bg-gray-200 dark:bg-gray-800 right-4" />
</div>)
}
</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

@@ -2,10 +2,13 @@ export * from "./nav-progress"
export * from "./nav-bar"
export * from "./fonts"
export * from "./input"
export * from "./home-nav-bar"
export * from "./home/nav-bar"
export * from "./home/machines"
export * from "./home/games"
export * from "./home/friends"
export * from "./game-card"
export * from "./tooltip"
export * from "./footer"
export * from "./react/footer"
export * from "./card"
export * from "./router-head"
export * from "./team-counter"

View File

@@ -94,7 +94,7 @@ export const HModalPanel = component$((props: PropsOf<'dialog'>) => {
const handleKeyDown$ = $((e: KeyboardEvent) => {
if (e.key === 'Escape') {
context.showSig.value = false;
// context.showSig.value = false;
e.stopPropagation();
}
});

View File

@@ -13,15 +13,12 @@ const navLinks = [
},
{
name: "Login",
href: "/auth/login"
}
]
type Props = {
link?: string
}
export const NavBar = component$(({ link }: Props) => {
export const NavBar = component$(() => {
const location = useLocation()
const hasScrolled = useSignal(false);
@@ -35,24 +32,16 @@ export const NavBar = component$(({ link }: Props) => {
return (
<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 · 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" >
<div class="px-4 mx-auto flex max-w-[600px] items-center sm:border-b-2 dark:border-gray-600 border-gray-400" >
<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" >
<h1 class="text-lg font-bricolage font-semibold" >
Nestri
</h1>
</Link>
<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">
<ul class="ml-0 -mr-4 flex font-medium m-4 font-mona tracking-tight flex-1 gap-1 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 ? 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")}>
<Link href={linkItem.href} prefetch={linkItem.name === "Login" && false} 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

@@ -44,7 +44,7 @@ export const ReactDisplay = ({
size,
weight,
align,
className: cn("font-title font-extrabold", className)
className: cn("font-basement font-extrabold", className)
})} {...props}>
<ReactBalancer.Balancer>
{children}

View File

@@ -0,0 +1,145 @@
/** @jsxImportSource react */
// import { FooterBanner } from "../footer-banner";
import { motion } from "framer-motion"
import { ReactDisplay } from "./display"
import { qwikify$ } from "@builder.io/qwik-react";
const transition = {
type: "spring",
stiffness: 100,
damping: 15,
restDelta: 0.001,
duration: 0.01,
}
type Props = {
children?: React.ReactNode;
}
export function ReactFooter({ children }: Props) {
return (
<>
<footer className="flex justify-center flex-col items-center w-screen pt-8 sm:pb-0 pb-8 [&>*]:w-full px-3">
<section className="mx-auto flex flex-col justify-center items-center max-w-[600px] pt-20">
<motion.img
initial={{
opacity: 0,
y: 120
}}
whileInView={{
y: 0,
opacity: 1
}}
viewport={{ once: true }}
transition={{
...transition
}}
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[50px] md:w-[80px] aspect-[90/69] select-none" />
<div className="my-4 sm:mt-8 w-full flex flex-col justify-center items-center">
<ReactDisplay className="mb-4 sm:text-[5.6rem] text-[3.2rem] text-balance text-center tracking-tight leading-none" >
<motion.span
initial={{
opacity: 0,
y: 100
}}
whileInView={{
y: 0,
opacity: 1
}}
transition={{
delay: 0.1,
...transition
}}
viewport={{ once: true }}
className="inline-block" >
Your games
</motion.span>
<motion.span
initial={{
opacity: 0,
y: 80
}}
transition={{
delay: 0.2,
...transition
}}
whileInView={{
y: 0,
opacity: 1
}}
viewport={{ once: true }}
className="inline-block" >
Your rules
</motion.span>
</ReactDisplay>
{/* <motion.p
initial={{
opacity: 0,
y: 50
}}
transition={{
delay: 0.3,
...transition
}}
whileInView={{
y: 0,
opacity: 1
}}
viewport={{ once: true }}
className="dark:text-gray-50/70 text-gray-950/70 text-base font-normal text-center leading-tight text-balance tracking-tight sm:text-xl"
>
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 running it on your own GPU instance
</motion.p> */}
<motion.div
initial={{
opacity: 0,
y: 60
}}
transition={{
delay: 0.4,
...transition
}}
whileInView={{
y: 0,
opacity: 1
}}
viewport={{ once: true }}
className="flex items-center justify-center mt-4 w-full"
>
{children}
</motion.div>
</div>
</section>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{
...transition,
duration: 0.8,
delay: 0.7
}}
className="w-full sm:flex z-[1] hidden pointer-events-none overflow-hidden -mt-[80px] justify-center items-center flex-col" >
<section className='my-0 bottom-0 text-[100%] max-w-[1440px] pointer-events-none w-full flex items-center translate-y-[40%] justify-center relative overflow-hidden px-2 z-10 [&_svg]:w-full [&_svg]:max-w-[1440px] [&_svg]:h-full [&_svg]:opacity-70' >
<svg viewBox="0 0 498.05 70.508" xmlns="http://www.w3.org/2000/svg" height={157} width={695} >
<g strokeLinecap="round" fillRule="evenodd" fontSize="9pt" stroke="currentColor" strokeWidth="0.25mm" fill="currentColor">
<path
fill="url(#paint1)"
pathLength="1"
stroke="url(#paint1)"
d="M 261.23 41.65 L 212.402 41.65 Q 195.313 41.65 195.313 27.002 L 195.313 14.795 A 17.814 17.814 0 0 1 196.311 8.57 Q 199.443 0.146 212.402 0.146 L 283.203 0.146 L 283.203 14.844 L 217.236 14.844 Q 215.337 14.844 214.945 16.383 A 3.67 3.67 0 0 0 214.844 17.285 L 214.844 24.561 Q 214.844 27.002 217.236 27.002 L 266.113 27.002 Q 283.203 27.002 283.203 41.65 L 283.203 53.857 A 17.814 17.814 0 0 1 282.205 60.083 Q 279.073 68.506 266.113 68.506 L 195.313 68.506 L 195.313 53.809 L 261.23 53.809 A 3.515 3.515 0 0 0 262.197 53.688 Q 263.672 53.265 263.672 51.367 L 263.672 44.092 A 3.515 3.515 0 0 0 263.551 43.126 Q 263.128 41.65 261.23 41.65 Z M 185.547 53.906 L 185.547 68.506 L 114.746 68.506 Q 97.656 68.506 97.656 53.857 L 97.656 14.795 A 17.814 17.814 0 0 1 98.655 8.57 Q 101.787 0.146 114.746 0.146 L 168.457 0.146 Q 185.547 0.146 185.547 14.795 L 185.547 31.885 A 17.827 17.827 0 0 1 184.544 38.124 Q 181.621 45.972 170.174 46.538 A 36.906 36.906 0 0 1 168.457 46.582 L 117.188 46.582 L 117.236 51.465 Q 117.236 53.906 119.629 53.955 L 185.547 53.906 Z M 19.531 14.795 L 19.531 68.506 L 0 68.506 L 0 0.146 L 70.801 0.146 Q 87.891 0.146 87.891 14.795 L 87.891 68.506 L 68.359 68.506 L 68.359 17.236 Q 68.359 14.795 65.967 14.795 L 19.531 14.795 Z M 449.219 68.506 L 430.176 46.533 L 400.391 46.533 L 400.391 68.506 L 380.859 68.506 L 380.859 0.146 L 451.66 0.146 A 24.602 24.602 0 0 1 458.423 0.994 Q 466.007 3.166 468.021 10.907 A 25.178 25.178 0 0 1 468.75 17.236 L 468.75 31.885 A 18.217 18.217 0 0 1 467.887 37.73 Q 465.954 43.444 459.698 45.455 A 23.245 23.245 0 0 1 454.492 46.436 L 473.633 68.506 L 449.219 68.506 Z M 292.969 0 L 371.094 0.098 L 371.094 14.795 L 341.846 14.795 L 341.846 68.506 L 322.266 68.506 L 322.217 14.795 L 292.969 14.844 L 292.969 0 Z M 478.516 0.146 L 498.047 0.146 L 498.047 68.506 L 478.516 68.506 L 478.516 0.146 Z M 400.391 14.844 L 400.391 31.885 L 446.826 31.885 Q 448.726 31.885 449.117 30.345 A 3.67 3.67 0 0 0 449.219 29.443 L 449.219 17.285 Q 449.219 14.844 446.826 14.844 L 400.391 14.844 Z M 117.188 31.836 L 163.574 31.934 Q 165.528 31.895 165.918 30.355 A 3.514 3.514 0 0 0 166.016 29.492 L 166.016 17.236 Q 166.016 15.337 164.476 14.945 A 3.67 3.67 0 0 0 163.574 14.844 L 119.629 14.795 Q 117.188 14.795 117.188 17.188 L 117.188 31.836 Z" />
</g>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" id="paint1" x1="317.5" x2="314.007" y1="-51.5" y2="126">
<stop stopColor="white"></stop>
<stop offset="1" stopOpacity="0"></stop>
</linearGradient>
</defs>
</svg>
</section>
</motion.div>
</footer>
</>
);
}
export const Footer = qwikify$(ReactFooter)

View File

@@ -20,8 +20,8 @@ type Props = {
export function ReactHeroSection({ children }: Props) {
return (
<>
<section className="px-4 w-screen" >
<header className="mx-auto max-w-xl pt-20 pb-1">
<section className="px-4 w-screen pt-10 sm:pt-0" >
<header className="mx-auto flex flex-col justify-center items-center max-w-[600px] pt-20 pb-1">
<motion.img
initial={{
opacity: 0,
@@ -35,9 +35,9 @@ export function ReactHeroSection({ children }: Props) {
transition={{
...transition
}}
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
<div className="my-4 sm:mt-8">
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[50px] md:w-[80px] aspect-[90/69] select-none" />
<div className="my-4 sm:mt-8 w-full flex flex-col justify-center items-center">
<ReactDisplay className="mb-4 sm:text-[5.6rem] text-[3.2rem] text-balance text-center tracking-tight leading-none" >
<motion.span
initial={{
opacity: 0,
@@ -53,7 +53,7 @@ export function ReactHeroSection({ children }: Props) {
}}
viewport={{ once: true }}
className="inline-block" >
Your games.
Your games
</motion.span>
<motion.span
initial={{
@@ -70,7 +70,7 @@ export function ReactHeroSection({ children }: Props) {
}}
viewport={{ once: true }}
className="inline-block" >
Your rules.
Your rules
</motion.span>
</ReactDisplay>
<motion.p
@@ -87,9 +87,9 @@ export function ReactHeroSection({ children }: Props) {
opacity: 1
}}
viewport={{ once: true }}
className="dark:text-gray-50/70 text-gray-950/70 text-lg font-normal tracking-tight sm:text-xl"
className="dark:text-gray-50/70 text-gray-950/70 text-base font-normal text-center leading-tight text-balance tracking-tight sm:text-xl"
>
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 on your own server.
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 running it on your own GPU instance
</motion.p>
<motion.div
initial={{

View File

@@ -24,7 +24,7 @@ export function ReactTitleSection({ title, description }: Props) {
return (
<>
<section className="px-4" >
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-4">
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-4 flex justify-center items-center flex-col">
<motion.img
initial={{
opacity: 0,
@@ -39,7 +39,7 @@ export function ReactTitleSection({ title, description }: Props) {
}}
viewport={{ once: true }}
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
<div className="my-4 sm:my-8">
<div className="my-4 sm:my-8 flex justify-center items-center flex-col text-center">
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
<motion.span
initial={{

File diff suppressed because one or more lines are too long