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 -->
This commit is contained in:
Wanjohi
2025-05-17 00:51:18 +03:00
committed by GitHub
parent cc2065299d
commit e1a903a7c9
82 changed files with 7819 additions and 1002 deletions

View File

@@ -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>

View File

@@ -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>
</>

View File

@@ -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>
)

View 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>
)
}

View File

@@ -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>
</>
)
}