⭐ feat(core): Implement Steam library sync with metadata extraction and image processing (#278)
## Description <!-- Briefly describe the purpose and scope of your changes --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added AWS queue infrastructure and SQS handler for processing Steam game libraries and images. - Introduced event-driven handling for new credentials and game additions, including image uploads to S3. - Added client functions to fetch Steam user libraries, friends lists, app info, and related images. - Added new database columns and schema updates to track game acquisition, playtime, and family sharing. - Added utility function for chunking arrays. - Added new event notifications for library queue processing and game creation. - Added new lookup functions for categories and teams by slug. - Introduced a new Team API with endpoints to list and fetch teams by slug. - Added a new Steam library page displaying game images. - **Enhancements** - Improved game creation with event notifications and upsert logic. - Enhanced category and team retrieval with new lookup functions. - Renamed and refined image categories for clearer classification. - Expanded dependencies for image processing and AWS SDK integration. - Improved image processing utilities with caching, ranking, and metadata extraction. - Refined Steam client utilities for concurrency and error handling. - **Bug Fixes** - Fixed event publishing timing and removed deprecated credential retrieval methods. - **Chores** - Updated infrastructure configurations with increased timeouts, memory, and resource linking. - Added new dependencies for image processing, caching, and AWS SDK clients. - Refined internal code structure and imports for clarity. - Removed Steam provider and related UI components from the frontend. - Disabled authentication providers and Steam-related routes in the frontend. - Updated API fetch handler to accept environment bindings. - **Refactor** - Simplified query result handling and renamed functions for better clarity. - Removed outdated event handler in favor of consolidated event subscriber. - Consolidated and simplified database relationships and permission queries. - **Tests** - No explicit test changes included in this release. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
@@ -92,23 +92,24 @@ export const App: Component = () => {
|
||||
const storage = useStorage();
|
||||
|
||||
return (
|
||||
<OpenAuthProvider
|
||||
issuer={import.meta.env.VITE_AUTH_URL}
|
||||
clientID="web"
|
||||
>
|
||||
// <OpenAuthProvider
|
||||
// issuer={import.meta.env.VITE_AUTH_URL}
|
||||
// clientID="web"
|
||||
// >
|
||||
<Root class={theme() === "light" ? lightClass : darkClass} id="styled">
|
||||
<Router>
|
||||
<Route
|
||||
path="*"
|
||||
component={(props) => (
|
||||
<AccountProvider
|
||||
loadingUI={
|
||||
<FullScreen>
|
||||
<Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
||||
</FullScreen>
|
||||
}>
|
||||
{props.children}
|
||||
</AccountProvider>
|
||||
// <AccountProvider
|
||||
// loadingUI={
|
||||
// <FullScreen>
|
||||
// <Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
||||
// </FullScreen>
|
||||
// }>
|
||||
// {props.children}
|
||||
props.children
|
||||
// </AccountProvider>
|
||||
)}
|
||||
>
|
||||
<Route path=":teamSlug">{TeamRoute}</Route>
|
||||
@@ -141,6 +142,6 @@ export const App: Component = () => {
|
||||
</Route>
|
||||
</Router>
|
||||
</Root>
|
||||
</OpenAuthProvider>
|
||||
// </OpenAuthProvider>
|
||||
)
|
||||
}
|
||||
BIN
packages/www/src/assets/games/1.png
Normal file
|
After Width: | Height: | Size: 625 KiB |
BIN
packages/www/src/assets/games/10.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
packages/www/src/assets/games/11.png
Normal file
|
After Width: | Height: | Size: 800 KiB |
BIN
packages/www/src/assets/games/12.png
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
packages/www/src/assets/games/13.png
Normal file
|
After Width: | Height: | Size: 779 KiB |
BIN
packages/www/src/assets/games/14.png
Normal file
|
After Width: | Height: | Size: 508 KiB |
BIN
packages/www/src/assets/games/15.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
packages/www/src/assets/games/16.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
packages/www/src/assets/games/17.png
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
packages/www/src/assets/games/18.png
Normal file
|
After Width: | Height: | Size: 706 KiB |
BIN
packages/www/src/assets/games/19.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
packages/www/src/assets/games/2.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
packages/www/src/assets/games/20.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
packages/www/src/assets/games/21.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
packages/www/src/assets/games/22.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
packages/www/src/assets/games/23.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
packages/www/src/assets/games/24.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
packages/www/src/assets/games/25.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
packages/www/src/assets/games/26.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
packages/www/src/assets/games/27.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
packages/www/src/assets/games/28.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
packages/www/src/assets/games/29.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
packages/www/src/assets/games/3.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
packages/www/src/assets/games/30.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
packages/www/src/assets/games/4.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
packages/www/src/assets/games/5.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
packages/www/src/assets/games/6.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
packages/www/src/assets/games/7.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
packages/www/src/assets/games/8.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
packages/www/src/assets/games/9.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
@@ -1,5 +1,5 @@
|
||||
import { animate, scroll } from "motion"
|
||||
import { A } from "@solidjs/router";
|
||||
import { A, useLocation } from "@solidjs/router";
|
||||
import { Container } from "@nestri/www/ui";
|
||||
import Avatar from "@nestri/www/ui/avatar";
|
||||
import { styled } from "@macaron-css/solid";
|
||||
@@ -200,6 +200,15 @@ const Nav = styled("nav", {
|
||||
}
|
||||
})
|
||||
|
||||
const capitalize = (name: string) => {
|
||||
return name
|
||||
.charAt(0) // first character
|
||||
.toUpperCase() // make it uppercase
|
||||
+ name
|
||||
.slice(1) // rest of the string
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the application's fixed top navigation bar with branding, team information, and navigation links.
|
||||
*
|
||||
@@ -230,7 +239,8 @@ export function Header(props: ParentProps) {
|
||||
})
|
||||
|
||||
// const account = useAccount()
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<NavWrapper scrolled={hasScrolled()}>
|
||||
@@ -313,6 +323,21 @@ export function Header(props: ParentProps) {
|
||||
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" />
|
||||
</DropIcon>
|
||||
</TeamRoot>
|
||||
{/**Fixme, this does not work for us */}
|
||||
<Show when={location.pathname.split("/").pop() !== "home"} >
|
||||
<LineSvg
|
||||
height="16"
|
||||
stroke-linejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width="16">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z" fill="currentColor"></path>
|
||||
</LineSvg>
|
||||
<div>{capitalize(location.pathname.split("/").pop()!)}</div>
|
||||
</Show>
|
||||
|
||||
</LogoRoot>
|
||||
{/* </Show> */}
|
||||
</Container>
|
||||
@@ -332,17 +357,17 @@ export function Header(props: ParentProps) {
|
||||
</NavLink>
|
||||
</NavRoot>
|
||||
</Show>
|
||||
<div style={{ "margin-bottom": "2px" }} >
|
||||
{/* <div style={{ "margin-bottom": "2px" }} >
|
||||
<AvatarImg src={"https://avatars.githubusercontent.com/u/71614375?v=4"} alt={`Wanjohi's avatar`} />
|
||||
{/* <Switch>
|
||||
<Switch>
|
||||
<Match when={account.current.avatarUrl} >
|
||||
<AvatarImg src={account.current.avatarUrl} alt={`${account.current.name}'s avatar`} />
|
||||
<AvatarImg src={account.current.avatarUrl} alt={`${account.current.name}'s avatar`} />
|
||||
</Match>
|
||||
<Match when={!account.current.avatarUrl}>
|
||||
<Avatar size={32} name={`${account.current.name}#${account.current.discriminator}`} />
|
||||
</Match>
|
||||
</Switch> */}
|
||||
</div>
|
||||
</Switch>
|
||||
</div> */}
|
||||
</RightRoot>
|
||||
</Nav>
|
||||
</NavWrapper>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FullScreen, theme } from "@nestri/www/ui";
|
||||
import { styled } from "@macaron-css/solid";
|
||||
import { Header } from "@nestri/www/pages/team/header";
|
||||
import { useSteam } from "@nestri/www/providers/steam";
|
||||
import { Modal } from "@nestri/www/ui/modal";
|
||||
import { createEffect, createSignal, Match, onCleanup, Switch } from "solid-js";
|
||||
import { Text } from "@nestri/www/ui/text"
|
||||
@@ -411,6 +410,7 @@ export function HomeRoute() {
|
||||
<Portal />
|
||||
</PortalContainer>
|
||||
</LastPlayedWrapper>
|
||||
*/}
|
||||
<GamesContainer>
|
||||
<GamesWrapper>
|
||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2024/05/15/acshadows-1715789601294.jpg" />
|
||||
@@ -508,7 +508,7 @@ export function HomeRoute() {
|
||||
</SteamGameContainer>
|
||||
</div>
|
||||
</SteamLibrary>
|
||||
</GamesContainer>*/}
|
||||
</GamesContainer>
|
||||
</FullScreen>
|
||||
</Header>
|
||||
</>
|
||||
|
||||
@@ -1,69 +1,66 @@
|
||||
import { HomeRoute } from "./home";
|
||||
import { LibraryRoute } from "./library";
|
||||
import { useOpenAuth } from "@openauthjs/solid";
|
||||
import { Route, useParams } from "@solidjs/router";
|
||||
import { ApiProvider } from "@nestri/www/providers/api";
|
||||
import { SteamRoute } from "@nestri/www/pages/team/steam";
|
||||
import { ZeroProvider } from "@nestri/www/providers/zero";
|
||||
import { TeamContext } from "@nestri/www/providers/context";
|
||||
import { SteamProvider } from "@nestri/www/providers/steam";
|
||||
import { createEffect, createMemo, Match, Switch } from "solid-js";
|
||||
import { NotAllowed, NotFound } from "@nestri/www/pages/not-found";
|
||||
import { useAccount, useStorage } from "@nestri/www/providers/account";
|
||||
|
||||
export const TeamRoute = (
|
||||
<Route
|
||||
component={(props) => {
|
||||
const params = useParams();
|
||||
const account = useAccount();
|
||||
const storage = useStorage();
|
||||
const openauth = useOpenAuth();
|
||||
// component={(props) => {
|
||||
// const params = useParams();
|
||||
// const account = useAccount();
|
||||
// const storage = useStorage();
|
||||
// const openauth = useOpenAuth();
|
||||
|
||||
const team = createMemo(() =>
|
||||
account.current.teams.find(
|
||||
(item) => item.slug === params.teamSlug,
|
||||
),
|
||||
);
|
||||
// const team = createMemo(() =>
|
||||
// account.current.teams.find(
|
||||
// (item) => item.slug === params.teamSlug,
|
||||
// ),
|
||||
// );
|
||||
|
||||
createEffect(() => {
|
||||
const t = team();
|
||||
if (!t) return;
|
||||
storage.set("team", t.id);
|
||||
});
|
||||
// createEffect(() => {
|
||||
// const t = team();
|
||||
// if (!t) return;
|
||||
// storage.set("team", t.id);
|
||||
// });
|
||||
|
||||
createEffect(() => {
|
||||
const teamSlug = params.teamSlug;
|
||||
for (const item of Object.values(account.all)) {
|
||||
for (const team of item.teams) {
|
||||
if (team.slug === teamSlug && item.id !== openauth.subject!.id) {
|
||||
openauth.switch(item.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// createEffect(() => {
|
||||
// const teamSlug = params.teamSlug;
|
||||
// for (const item of Object.values(account.all)) {
|
||||
// for (const team of item.teams) {
|
||||
// if (team.slug === teamSlug && item.id !== openauth.subject!.id) {
|
||||
// openauth.switch(item.email);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={!team()}>
|
||||
{/* TODO: Add a public page for (other) teams */}
|
||||
<NotAllowed header />
|
||||
</Match>
|
||||
<Match when={team()}>
|
||||
<TeamContext.Provider value={() => team()!}>
|
||||
<ZeroProvider>
|
||||
<ApiProvider>
|
||||
<SteamProvider>
|
||||
{props.children}
|
||||
</SteamProvider>
|
||||
</ApiProvider>
|
||||
</ZeroProvider>
|
||||
</TeamContext.Provider>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}}
|
||||
>
|
||||
// return (
|
||||
// <Switch>
|
||||
// <Match when={!team()}>
|
||||
// {/* TODO: Add a public page for (other) teams */}
|
||||
// <NotAllowed header />
|
||||
// </Match>
|
||||
// <Match when={team()}>
|
||||
// <TeamContext.Provider value={() => team()!}>
|
||||
// <ZeroProvider>
|
||||
// <ApiProvider>
|
||||
// {props.children}
|
||||
// </ApiProvider>
|
||||
// </ZeroProvider>
|
||||
// </TeamContext.Provider>
|
||||
// </Match>
|
||||
// </Switch>
|
||||
// )
|
||||
// }}
|
||||
>
|
||||
<Route path="" component={HomeRoute} />
|
||||
<Route path="steam" component={SteamRoute} />
|
||||
<Route path="library" component={LibraryRoute} />
|
||||
<Route path="*" component={() => <NotFound header />} />
|
||||
</Route>
|
||||
)
|
||||
179
packages/www/src/pages/team/library.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { For } from "solid-js";
|
||||
import { styled } from "@macaron-css/solid";
|
||||
import { FullScreen, theme } from "@nestri/www/ui";
|
||||
import { Header } from "@nestri/www/pages/team/header";
|
||||
|
||||
const Container = styled("div", {
|
||||
base: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
zIndex: 10,
|
||||
isolation: "isolate",
|
||||
marginTop: 30,
|
||||
}
|
||||
})
|
||||
|
||||
const Wrapper = styled("div", {
|
||||
base: {
|
||||
maxWidth: "70vw",
|
||||
width: "100%",
|
||||
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
||||
margin: "0 auto",
|
||||
display: "grid",
|
||||
columnGap: 12,
|
||||
rowGap: 10
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const SquareImage = styled("img", {
|
||||
base: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
userSelect: "none",
|
||||
aspectRatio: "1/1",
|
||||
borderRadius: 10,
|
||||
transitionDuration: "0.4s",
|
||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
||||
transitionProperty: "opacity",
|
||||
cursor: "pointer",
|
||||
border: `3px solid transparent`,
|
||||
":hover": {
|
||||
// transform: "scale(1.01)",
|
||||
outline: `3px solid ${theme.color.brand}`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const TitleHeader = styled("header", {
|
||||
base: {
|
||||
borderBottom: `1px solid ${theme.color.gray.d400}`,
|
||||
color: theme.color.d1000.gray
|
||||
}
|
||||
})
|
||||
|
||||
const TitleWrapper = styled("div", {
|
||||
base: {
|
||||
width: "calc(1000px + calc(2 * 24px))",
|
||||
paddingLeft: "24px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
paddingRight: "24px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
maxWidth: "100%"
|
||||
}
|
||||
})
|
||||
|
||||
const TitleContainer = styled("div", {
|
||||
base: {
|
||||
margin: "40px 0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
width: "100%",
|
||||
minWidth: 0
|
||||
}
|
||||
})
|
||||
|
||||
const Title = styled("h1", {
|
||||
base: {
|
||||
lineHeight: "2.5rem",
|
||||
fontWeight: theme.font.weight.semibold,
|
||||
letterSpacing: "-0.069375rem",
|
||||
textAlign: "left",
|
||||
fontSize: theme.font.size["4xl"],
|
||||
textTransform: "capitalize"
|
||||
}
|
||||
})
|
||||
|
||||
const Description = styled("p", {
|
||||
base: {
|
||||
fontSize: theme.font.size.sm,
|
||||
lineHeight: "1.25rem",
|
||||
textAlign: "left",
|
||||
fontWeight: theme.font.weight.regular,
|
||||
letterSpacing: "initial",
|
||||
color: theme.color.gray.d900
|
||||
}
|
||||
})
|
||||
|
||||
const LogoFooter = styled("section", {
|
||||
base: {
|
||||
position: "relative",
|
||||
bottom: -1,
|
||||
fontSize: "100%",
|
||||
maxWidth: 1440,
|
||||
width: "100%",
|
||||
pointerEvents: "none",
|
||||
display: "flex",
|
||||
margin: "-80px 0",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0 8px",
|
||||
overflow: "hidden",
|
||||
},
|
||||
})
|
||||
|
||||
const Logo = styled("svg", {
|
||||
base: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transform: "translateY(40%)",
|
||||
opacity: "70%",
|
||||
}
|
||||
})
|
||||
//MaRt@6563
|
||||
export function LibraryRoute() {
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<FullScreen inset="header" >
|
||||
<TitleHeader>
|
||||
<TitleWrapper>
|
||||
<TitleContainer>
|
||||
<Title>
|
||||
Your Steam Library
|
||||
</Title>
|
||||
<Description>
|
||||
Install games directly from your Steam account to your Nestri Machine
|
||||
</Description>
|
||||
</TitleContainer>
|
||||
</TitleWrapper>
|
||||
</TitleHeader>
|
||||
<Container>
|
||||
<Wrapper>
|
||||
<For each={new Array(30)} >
|
||||
{(item, index) => (
|
||||
<SquareImage
|
||||
draggable={false}
|
||||
alt="Assasin's Creed Shadows"
|
||||
src={`/src/assets/games/${index() + 1}.png`} />
|
||||
)}
|
||||
</For>
|
||||
</Wrapper>
|
||||
</Container>
|
||||
<LogoFooter >
|
||||
<Logo viewBox="0 0 498.05 70.508" xmlns="http://www.w3.org/2000/svg" height={157} width={695} >
|
||||
<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>
|
||||
</Logo>
|
||||
</LogoFooter>
|
||||
</FullScreen>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
import { Header } from "./header"
|
||||
import { theme } from "@nestri/www/ui";
|
||||
import { Text } from "@nestri/www/ui";
|
||||
import { styled } from "@macaron-css/solid";
|
||||
import { useSteam } from "@nestri/www/providers/steam";
|
||||
import { createEffect, onCleanup } from "solid-js";
|
||||
|
||||
// FIXME: Remove this route, or move it to machines
|
||||
|
||||
// The idea has changed, let the user login to Steam from the / route
|
||||
// Let the machines route remain different from the main page
|
||||
// Why? It becomes much simpler for routing and onboarding, plus how often will you move to the machines route?
|
||||
// Now it will be the home page's problem with making sure the user can download and install games on whatever machine they need/want
|
||||
|
||||
const Root = styled("div", {
|
||||
base: {
|
||||
display: "grid",
|
||||
gridAutoRows: "1fr",
|
||||
position: "relative",
|
||||
gridTemplateRows: "0 auto",
|
||||
backgroundColor: theme.color.background.d200,
|
||||
minHeight: `calc(100vh - ${theme.headerHeight.root})`,
|
||||
gridTemplateColumns: "minmax(24px,1fr) minmax(0,1000px) minmax(24px,1fr)"
|
||||
},
|
||||
});
|
||||
|
||||
const Section = styled("section", {
|
||||
base: {
|
||||
gridColumn: "1/-1",
|
||||
}
|
||||
})
|
||||
|
||||
const TitleHeader = styled("header", {
|
||||
base: {
|
||||
borderBottom: `1px solid ${theme.color.gray.d400}`,
|
||||
color: theme.color.d1000.gray
|
||||
}
|
||||
})
|
||||
|
||||
const TitleWrapper = styled("div", {
|
||||
base: {
|
||||
width: "calc(1000px + calc(2 * 24px))",
|
||||
paddingLeft: "24px",
|
||||
display: "flex",
|
||||
paddingRight: "24px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
maxWidth: "100%"
|
||||
}
|
||||
})
|
||||
|
||||
const TitleContainer = styled("div", {
|
||||
base: {
|
||||
margin: "40px 0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
width: "100%",
|
||||
minWidth: 0
|
||||
}
|
||||
})
|
||||
|
||||
const ButtonContainer = styled("div", {
|
||||
base: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 16,
|
||||
margin: "40px 0",
|
||||
}
|
||||
})
|
||||
|
||||
const Title = styled("h1", {
|
||||
base: {
|
||||
lineHeight: "2.5rem",
|
||||
fontWeight: theme.font.weight.semibold,
|
||||
letterSpacing: "-0.069375rem",
|
||||
fontSize: theme.font.size["4xl"],
|
||||
textTransform: "capitalize"
|
||||
}
|
||||
})
|
||||
|
||||
const Description = styled("p", {
|
||||
base: {
|
||||
fontSize: theme.font.size.sm,
|
||||
lineHeight: "1.25rem",
|
||||
fontWeight: theme.font.weight.regular,
|
||||
letterSpacing: "initial",
|
||||
color: theme.color.gray.d900
|
||||
}
|
||||
})
|
||||
|
||||
const QRButton = styled("button", {
|
||||
base: {
|
||||
height: 40,
|
||||
borderRadius: theme.borderRadius,
|
||||
backgroundColor: theme.color.d1000.gray,
|
||||
color: theme.color.gray.d100,
|
||||
fontSize: theme.font.size.sm,
|
||||
textWrap: "nowrap",
|
||||
border: "1px solid transparent",
|
||||
padding: `${theme.space[2]} ${theme.space[4]}`,
|
||||
letterSpacing: 0.1,
|
||||
lineHeight: "1.25rem",
|
||||
fontFamily: theme.font.family.body,
|
||||
fontWeight: theme.font.weight.medium,
|
||||
cursor: "pointer",
|
||||
transitionDelay: "0s, 0s",
|
||||
transitionDuration: "0.2s, 0.2s",
|
||||
transitionProperty: "background-color, border",
|
||||
transitionTimingFunction: "ease-out, ease-out",
|
||||
display: "inline-flex",
|
||||
gap: theme.space[2],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
":disabled": {
|
||||
pointerEvents: "none",
|
||||
},
|
||||
":hover": {
|
||||
background: theme.color.hoverColor
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ButtonText = styled("span", {
|
||||
base: {
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
}
|
||||
})
|
||||
|
||||
const Body = styled("div", {
|
||||
base: {
|
||||
padding: "0 24px",
|
||||
width: "calc(1000px + calc(2 * 24px))",
|
||||
minWidth: "calc(100vh - 273px)",
|
||||
margin: "24px auto"
|
||||
}
|
||||
})
|
||||
|
||||
const GamesContainer = styled("div", {
|
||||
base: {
|
||||
background: theme.color.background.d200,
|
||||
padding: "32px 16px",
|
||||
borderRadius: 5,
|
||||
border: `1px solid ${theme.color.gray.d400}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "calc(100vh - 300px)",
|
||||
}
|
||||
})
|
||||
|
||||
const EmptyState = styled("div", {
|
||||
base: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: theme.space[8],
|
||||
flexDirection: "column"
|
||||
}
|
||||
})
|
||||
|
||||
const SteamLogoContainer = styled("div", {
|
||||
base: {
|
||||
height: 60,
|
||||
width: 60,
|
||||
padding: 4,
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.color.background.d200,
|
||||
border: `1px solid ${theme.color.gray.d400}`,
|
||||
}
|
||||
})
|
||||
export function SteamRoute() {
|
||||
const steam = useSteam();
|
||||
|
||||
createEffect(() => {
|
||||
// steam.client.loginStream.connect();
|
||||
|
||||
// Clean up on component unmount
|
||||
// onCleanup(() => {
|
||||
// steam.client.loginStream.disconnect();
|
||||
// });
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Root>
|
||||
<Section>
|
||||
<TitleHeader>
|
||||
<TitleWrapper>
|
||||
<TitleContainer>
|
||||
<Title>
|
||||
Steam Library
|
||||
</Title>
|
||||
<Description>
|
||||
{/* Read and write directly to databases and stores from your projects. */}
|
||||
Install games directly from your Steam account to your Nestri Machine
|
||||
</Description>
|
||||
</TitleContainer>
|
||||
<ButtonContainer>
|
||||
<QRButton>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 32 32">
|
||||
<path fill="currentColor" d="M15.974 0C7.573 0 .682 6.479.031 14.714l8.573 3.547a4.5 4.5 0 0 1 2.552-.786c.083 0 .167.005.25.005l3.813-5.521v-.078c0-3.328 2.703-6.031 6.031-6.031s6.036 2.708 6.036 6.036a6.04 6.04 0 0 1-6.036 6.031h-.135l-5.438 3.88c0 .073.005.141.005.214c0 2.5-2.021 4.526-4.521 4.526c-2.177 0-4.021-1.563-4.443-3.635L.583 20.36c1.901 6.719 8.063 11.641 15.391 11.641c8.833 0 15.995-7.161 15.995-16s-7.161-16-15.995-16zm-5.922 24.281l-1.964-.813a3.4 3.4 0 0 0 1.755 1.667a3.404 3.404 0 0 0 4.443-1.833a3.38 3.38 0 0 0 .005-2.599a3.36 3.36 0 0 0-1.839-1.844a3.38 3.38 0 0 0-2.5-.042l2.026.839c1.276.536 1.88 2 1.349 3.276s-2 1.88-3.276 1.349zm15.219-12.406a4.025 4.025 0 0 0-4.016-4.021a4.02 4.02 0 1 0 0 8.042a4.02 4.02 0 0 0 4.016-4.021m-7.026-.005c0-1.672 1.349-3.021 3.016-3.021s3.026 1.349 3.026 3.021c0 1.667-1.359 3.021-3.026 3.021s-3.016-1.354-3.016-3.021" />
|
||||
</svg>
|
||||
<ButtonText>
|
||||
Connect Steam
|
||||
</ButtonText>
|
||||
</QRButton>
|
||||
</ButtonContainer>
|
||||
</TitleWrapper>
|
||||
</TitleHeader>
|
||||
<Body>
|
||||
<GamesContainer>
|
||||
<EmptyState>
|
||||
<SteamLogoContainer>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path fill="currentColor" d="M15.974 0C7.573 0 .682 6.479.031 14.714l8.573 3.547a4.5 4.5 0 0 1 2.552-.786c.083 0 .167.005.25.005l3.813-5.521v-.078c0-3.328 2.703-6.031 6.031-6.031s6.036 2.708 6.036 6.036a6.04 6.04 0 0 1-6.036 6.031h-.135l-5.438 3.88c0 .073.005.141.005.214c0 2.5-2.021 4.526-4.521 4.526c-2.177 0-4.021-1.563-4.443-3.635L.583 20.36c1.901 6.719 8.063 11.641 15.391 11.641c8.833 0 15.995-7.161 15.995-16s-7.161-16-15.995-16zm-5.922 24.281l-1.964-.813a3.4 3.4 0 0 0 1.755 1.667a3.404 3.404 0 0 0 4.443-1.833a3.38 3.38 0 0 0 .005-2.599a3.36 3.36 0 0 0-1.839-1.844a3.38 3.38 0 0 0-2.5-.042l2.026.839c1.276.536 1.88 2 1.349 3.276s-2 1.88-3.276 1.349zm15.219-12.406a4.025 4.025 0 0 0-4.016-4.021a4.02 4.02 0 1 0 0 8.042a4.02 4.02 0 0 0 4.016-4.021m-7.026-.005c0-1.672 1.349-3.021 3.016-3.021s3.026 1.349 3.026 3.021c0 1.667-1.359 3.021-3.026 3.021s-3.016-1.354-3.016-3.021" />
|
||||
</svg>
|
||||
</SteamLogoContainer>
|
||||
<Text align="center" style={{ "letter-spacing": "-0.3px" }} size="base" >
|
||||
{/* After connecting your Steam account, your games will appear here */}
|
||||
{/* URL: {steam.client.loginStream.loginUrl()} */}
|
||||
</Text>
|
||||
</EmptyState>
|
||||
</GamesContainer>
|
||||
</Body>
|
||||
</Section>
|
||||
</Root>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import { useTeam } from "./context";
|
||||
import { EventSource } from 'eventsource'
|
||||
import { useOpenAuth } from "@openauthjs/solid";
|
||||
import { createSignal, onCleanup } from "solid-js";
|
||||
import { createInitializedContext } from "../common/context";
|
||||
|
||||
// Global connection state to prevent multiple instances
|
||||
let globalEventSource: EventSource | null = null;
|
||||
let globalReconnectAttempts = 0;
|
||||
const MAX_RECONNECT_ATTEMPTS = 1;
|
||||
let isConnecting = false;
|
||||
let activeConnection: SteamConnection | null = null;
|
||||
|
||||
// FIXME: The redo button is not working as expected... it does not reinitialise the connection
|
||||
|
||||
// Type definitions for the events
|
||||
interface SteamEventTypes {
|
||||
'connected': { sessionID: string };
|
||||
'challenge': { sessionID: string; url: string };
|
||||
'error': { message: string };
|
||||
'completed': { sessionID: string };
|
||||
}
|
||||
|
||||
// Type for the connection
|
||||
type SteamConnection = {
|
||||
addEventListener: <T extends keyof SteamEventTypes>(
|
||||
event: T,
|
||||
callback: (data: SteamEventTypes[T]) => void
|
||||
) => () => void;
|
||||
removeEventListener: <T extends keyof SteamEventTypes>(
|
||||
event: T,
|
||||
callback: (data: SteamEventTypes[T]) => void
|
||||
) => void;
|
||||
disconnect: () => void;
|
||||
isConnected: () => boolean;
|
||||
}
|
||||
|
||||
interface SteamContext {
|
||||
ready: boolean;
|
||||
client: {
|
||||
// SSE connection for login
|
||||
login: {
|
||||
connect: () => Promise<SteamConnection>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Create the initialized context
|
||||
export const { use: useSteam, provider: SteamProvider } = createInitializedContext(
|
||||
"SteamContext",
|
||||
() => {
|
||||
const team = useTeam();
|
||||
const auth = useOpenAuth();
|
||||
|
||||
// Create the HTTP client for regular endpoints
|
||||
const client = {
|
||||
// SSE connection factory for login
|
||||
login: {
|
||||
connect: async (): Promise<SteamConnection> => {
|
||||
// Return existing connection if active
|
||||
if (activeConnection && globalEventSource && globalEventSource.readyState !== 2) {
|
||||
return activeConnection;
|
||||
}
|
||||
|
||||
// Prevent multiple simultaneous connection attempts
|
||||
if (isConnecting) {
|
||||
console.log("Connection attempt already in progress, waiting...");
|
||||
// Wait for existing connection attempt to finish
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!isConnecting && activeConnection) {
|
||||
clearInterval(checkInterval);
|
||||
resolve(activeConnection);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
|
||||
const [isConnected, setIsConnected] = createSignal(false);
|
||||
|
||||
// Store event listeners
|
||||
const listeners: Record<string, Array<(data: any) => void>> = {
|
||||
'connected': [],
|
||||
'challenge': [],
|
||||
'error': [],
|
||||
'completed': []
|
||||
};
|
||||
|
||||
// Method to add event listeners
|
||||
const addEventListener = <T extends keyof SteamEventTypes>(
|
||||
event: T,
|
||||
callback: (data: SteamEventTypes[T]) => void
|
||||
) => {
|
||||
if (!listeners[event]) {
|
||||
listeners[event] = [];
|
||||
}
|
||||
|
||||
listeners[event].push(callback as any);
|
||||
|
||||
// Return a function to remove this specific listener
|
||||
return () => {
|
||||
removeEventListener(event, callback);
|
||||
};
|
||||
};
|
||||
|
||||
// Method to remove event listeners
|
||||
const removeEventListener = <T extends keyof SteamEventTypes>(
|
||||
event: T,
|
||||
callback: (data: SteamEventTypes[T]) => void
|
||||
) => {
|
||||
if (listeners[event]) {
|
||||
const index = listeners[event].indexOf(callback as any);
|
||||
if (index !== -1) {
|
||||
listeners[event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle notifying listeners safely
|
||||
const notifyListeners = (eventType: string, data: any) => {
|
||||
if (listeners[eventType]) {
|
||||
listeners[eventType].forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in ${eventType} event handler:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize connection
|
||||
const initConnection = async () => {
|
||||
if (globalReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.log(`Maximum reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);
|
||||
notifyListeners('error', { message: 'Connection to Steam authentication failed after multiple attempts' });
|
||||
isConnecting = false;
|
||||
disconnect()
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalEventSource) {
|
||||
globalEventSource.close();
|
||||
globalEventSource = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await auth.access();
|
||||
|
||||
// Create new EventSource connection
|
||||
globalEventSource = new EventSource(`${import.meta.env.VITE_API_URL}/steam/login`, {
|
||||
fetch: (input, init) =>
|
||||
fetch(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'x-nestri-team': team().id
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
globalEventSource.onopen = () => {
|
||||
console.log('Connected to Steam login stream');
|
||||
setIsConnected(true);
|
||||
globalReconnectAttempts = 0; // Reset reconnect counter on successful connection
|
||||
isConnecting = false;
|
||||
};
|
||||
|
||||
// Set up event handlers for all specific events
|
||||
['connected', 'challenge', 'completed'].forEach((eventType) => {
|
||||
globalEventSource!.addEventListener(eventType, (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(`Received ${eventType} event:`, data);
|
||||
notifyListeners(eventType, data);
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${eventType} event data:`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle connection errors (this is different from server-sent 'error' events)
|
||||
globalEventSource.onerror = (error) => {
|
||||
console.error('Steam login stream connection error:', error);
|
||||
setIsConnected(false);
|
||||
|
||||
// Close the connection to prevent automatic browser reconnect
|
||||
if (globalEventSource) {
|
||||
globalEventSource.close();
|
||||
}
|
||||
|
||||
// Check if we should attempt to reconnect
|
||||
if (globalReconnectAttempts <= MAX_RECONNECT_ATTEMPTS) {
|
||||
const currentAttempt = globalReconnectAttempts + 1;
|
||||
console.log(`Reconnecting (attempt ${currentAttempt}/${MAX_RECONNECT_ATTEMPTS})...`);
|
||||
globalReconnectAttempts = currentAttempt;
|
||||
|
||||
// Exponential backoff for reconnection
|
||||
const delay = Math.min(1000 * Math.pow(2, globalReconnectAttempts), 30000);
|
||||
setTimeout(initConnection, delay);
|
||||
} else {
|
||||
console.error(`Maximum reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
|
||||
// Notify listeners about connection failure
|
||||
notifyListeners('error', { message: 'Connection to Steam authentication failed after multiple attempts' });
|
||||
disconnect();
|
||||
isConnecting = false;
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Steam login stream:', error);
|
||||
setIsConnected(false);
|
||||
isConnecting = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnection function
|
||||
const disconnect = () => {
|
||||
if (globalEventSource) {
|
||||
globalEventSource.close();
|
||||
globalEventSource = null;
|
||||
setIsConnected(false);
|
||||
console.log('Disconnected from Steam login stream');
|
||||
|
||||
// Clear all listeners
|
||||
Object.keys(listeners).forEach(key => {
|
||||
listeners[key] = [];
|
||||
});
|
||||
|
||||
activeConnection = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Start the connection immediately
|
||||
await initConnection();
|
||||
|
||||
// Create the connection interface
|
||||
const connection: SteamConnection = {
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
disconnect,
|
||||
isConnected: () => isConnected()
|
||||
};
|
||||
|
||||
// Store the active connection
|
||||
activeConnection = connection;
|
||||
|
||||
// Clean up on context destruction
|
||||
onCleanup(() => {
|
||||
// Instead of disconnecting on cleanup, we'll leave the connection
|
||||
// active for other components to use
|
||||
// Only disconnect if no components are using it
|
||||
if (!isConnected()) {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
client,
|
||||
ready: true
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useTeam } from "./context"
|
||||
import { createEffect } from "solid-js"
|
||||
// import { createEffect } from "solid-js"
|
||||
import { schema } from "@nestri/zero/schema"
|
||||
import { useQuery } from "@rocicorp/zero/solid"
|
||||
// import { useQuery } from "@rocicorp/zero/solid"
|
||||
import { useOpenAuth } from "@openauthjs/solid"
|
||||
import { Query, Schema, Zero } from "@rocicorp/zero"
|
||||
import { Zero } from "@rocicorp/zero"
|
||||
import { useAccount } from "@nestri/www/providers/account"
|
||||
import { createInitializedContext } from "@nestri/www/common/context"
|
||||
|
||||
export const { use: useZero, provider: ZeroProvider } =
|
||||
createInitializedContext("ZeroContext", () => {
|
||||
const team = useTeam()
|
||||
const auth = useOpenAuth()
|
||||
const account = useAccount()
|
||||
const team = useTeam()
|
||||
const zero = new Zero({
|
||||
schema: schema,
|
||||
auth: () => auth.access(),
|
||||
userID: account.current.email,
|
||||
schema,
|
||||
storageKey: team().id,
|
||||
auth: () => auth.access(),
|
||||
userID: account.current.id,
|
||||
server: import.meta.env.VITE_ZERO_URL,
|
||||
})
|
||||
|
||||
@@ -28,12 +28,12 @@ export const { use: useZero, provider: ZeroProvider } =
|
||||
};
|
||||
});
|
||||
|
||||
export function usePersistentQuery<TSchema extends Schema, TTable extends keyof TSchema['tables'] & string, TReturn>(querySignal: () => Query<TSchema, TTable, TReturn>) {
|
||||
const team = useTeam()
|
||||
//@ts-ignore
|
||||
const q = () => querySignal().where("team_id", "=", team().id).where("time_deleted", "IS", null)
|
||||
createEffect(() => {
|
||||
q().preload()
|
||||
})
|
||||
return useQuery<TSchema, TTable, TReturn>(q)
|
||||
}
|
||||
// export function usePersistentQuery<TSchema extends Schema, TTable extends keyof TSchema['tables'] & string, TReturn>(querySignal: () => Query<TSchema, TTable, TReturn>) {
|
||||
// const team = useTeam()
|
||||
// //@ts-ignore
|
||||
// const q = () => querySignal().where("team_id", "=", team().id).where("time_deleted", "IS", null)
|
||||
// createEffect(() => {
|
||||
// q().preload()
|
||||
// })
|
||||
// return useQuery<TSchema, TTable, TReturn>(q)
|
||||
// }
|
||||
@@ -6,6 +6,7 @@ export const FullScreen = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position:"relative",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
justifyContent: "center"
|
||||
@@ -15,7 +16,7 @@ export const FullScreen = styled("div", {
|
||||
none: {},
|
||||
header: {
|
||||
paddingTop: `calc(1px + ${theme.headerHeight.root})`,
|
||||
minHeight: `calc(100dvh - ${theme.headerHeight.root})`,
|
||||
// minHeight: `calc(100dvh - ${theme.headerHeight.root})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||