mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 00:35:38 +02:00
Compare commits
4 Commits
17fb572b87
...
feat/home
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b3043436 | ||
|
|
623a0db97f | ||
|
|
9827100d19 | ||
|
|
88f499ba5e |
18
infra/cdn.ts
Normal file
18
infra/cdn.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { domain } from "./dns";
|
||||||
|
import { storage } from "./storage";
|
||||||
|
|
||||||
|
export const cdn = new sst.aws.Router("CDNRouter", {
|
||||||
|
routes: {
|
||||||
|
"/*": {
|
||||||
|
bucket: storage,
|
||||||
|
rewrite: {
|
||||||
|
regex: "^/([a-zA-Z0-9_-]+)$",
|
||||||
|
to: "/public/$1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
name: "cdn." + domain,
|
||||||
|
dns: sst.cloudflare.dns()
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1 +1,5 @@
|
|||||||
export const storage = new sst.aws.Bucket("Storage");
|
export const storage = new sst.aws.Bucket("Storage", {
|
||||||
|
access: "cloudfront"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zeroStorage = new sst.aws.Bucket("ZeroStorage");
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// This is the website part where people play and connect
|
// This is the website part where people play and connect
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
|
import { cdn } from "./cdn";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { zero } from "./zero";
|
import { zero } from "./zero";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
@@ -16,6 +17,7 @@ new sst.aws.StaticSite("Web", {
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
VITE_API_URL: api.url,
|
VITE_API_URL: api.url,
|
||||||
|
VITE_CDN_URL: cdn.url,
|
||||||
VITE_STAGE: $app.stage,
|
VITE_STAGE: $app.stage,
|
||||||
VITE_AUTH_URL: auth.url,
|
VITE_AUTH_URL: auth.url,
|
||||||
VITE_ZERO_URL: zero.url,
|
VITE_ZERO_URL: zero.url,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { auth } from "./auth";
|
|||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { cluster } from "./cluster";
|
import { cluster } from "./cluster";
|
||||||
import { storage } from "./storage";
|
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
import { zeroStorage } from "./storage";
|
||||||
|
|
||||||
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
|
const connectionString = $interpolate`postgresql://${postgres.username}:${postgres.password}@${postgres.host}:${postgres.port}/${postgres.database}`;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ const zeroEnv = {
|
|||||||
? {
|
? {
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${storage.name}/zero/0`,
|
ZERO_LITESTREAM_BACKUP_URL: $interpolate`s3://${zeroStorage.name}/zero/0`,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const replicationManager = !$dev
|
|||||||
capacity: "spot",
|
capacity: "spot",
|
||||||
architecture: "arm64",
|
architecture: "arm64",
|
||||||
image: zeroEnv.ZERO_IMAGE_URL,
|
image: zeroEnv.ZERO_IMAGE_URL,
|
||||||
link: [storage, postgres],
|
link: [zeroStorage, postgres],
|
||||||
health: {
|
health: {
|
||||||
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
|
command: ["CMD-SHELL", "curl -f http://localhost:4849/ || exit 1"],
|
||||||
interval: "5 seconds",
|
interval: "5 seconds",
|
||||||
@@ -123,7 +123,7 @@ const replicationManager = !$dev
|
|||||||
export const zero = new sst.aws.Service("Zero", {
|
export const zero = new sst.aws.Service("Zero", {
|
||||||
cluster,
|
cluster,
|
||||||
image: zeroEnv.ZERO_IMAGE_URL,
|
image: zeroEnv.ZERO_IMAGE_URL,
|
||||||
link: [storage, postgres],
|
link: [zeroStorage, postgres],
|
||||||
architecture: "arm64",
|
architecture: "arm64",
|
||||||
cpu: "0.5 vCPU",
|
cpu: "0.5 vCPU",
|
||||||
memory: "1 GB",
|
memory: "1 GB",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
@@ -91,7 +91,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
@@ -136,7 +136,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new HeadObjectCommand({
|
new HeadObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ export const handler = bus.subscriber(
|
|||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: Resource.Storage.name,
|
Bucket: Resource.Storage.name,
|
||||||
Key: `images/${image.hash}`,
|
Key: `public/${image.hash}`,
|
||||||
Body: image.buffer,
|
Body: image.buffer,
|
||||||
...(image.format && { ContentType: `image/${image.format}` }),
|
...(image.format && { ContentType: `image/${image.format}` }),
|
||||||
Metadata: {
|
Metadata: {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const App: Component = () => {
|
|||||||
: "light",
|
: "light",
|
||||||
);
|
);
|
||||||
|
|
||||||
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const setColorScheme = (e: MediaQueryListEvent) => {
|
const setColorScheme = (e: MediaQueryListEvent) => {
|
||||||
setTheme(e.matches ? "dark" : "light");
|
setTheme(e.matches ? "dark" : "light");
|
||||||
};
|
};
|
||||||
@@ -94,26 +94,26 @@ export const App: Component = () => {
|
|||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAuthProvider
|
// <OpenAuthProvider
|
||||||
issuer={import.meta.env.VITE_AUTH_URL}
|
// issuer={import.meta.env.VITE_AUTH_URL}
|
||||||
clientID="web"
|
// clientID="web"
|
||||||
>
|
// >
|
||||||
<Root class={theme() === "light" ? lightClass : darkClass}>
|
<Root class={theme() === "light" ? lightClass : darkClass}>
|
||||||
<Router>
|
<Router>
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
component={(props) => (
|
component={(props) => (
|
||||||
<AccountProvider
|
// <AccountProvider
|
||||||
loadingUI={
|
// loadingUI={
|
||||||
<FullScreen>
|
// <FullScreen>
|
||||||
<Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
// <Text weight='semibold' spacing='xs' size="3xl" font="heading" >Confirming your identity…</Text>
|
||||||
</FullScreen>
|
// </FullScreen>
|
||||||
}>
|
// }>
|
||||||
<ZeroProvider>
|
// <ZeroProvider>
|
||||||
{/* props.children */}
|
props.children
|
||||||
{props.children}
|
// {props.children}
|
||||||
</ZeroProvider>
|
// </ZeroProvider>
|
||||||
</AccountProvider>
|
// </AccountProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Route path=":steamID">{SteamRoute}</Route>
|
<Route path=":steamID">{SteamRoute}</Route>
|
||||||
@@ -145,6 +145,6 @@ export const App: Component = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
</Root>
|
</Root>
|
||||||
</OpenAuthProvider>
|
// </OpenAuthProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
54
packages/www/src/assets/service-worker.js
Normal file
54
packages/www/src/assets/service-worker.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const CACHE_NAME = 'image-cache-v1';
|
||||||
|
const AUTH_TOKEN = 'Bearer YOUR_DYNAMIC_AUTH_TOKEN'; // Replace at runtime if needed
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting(); // Activate immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
clients.claim(); // Take control of all clients
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const req = event.request;
|
||||||
|
|
||||||
|
// Only intercept image requests
|
||||||
|
if (req.destination !== 'image') return;
|
||||||
|
|
||||||
|
// Only intercept our image requests
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (import.meta.env.VITE_CDN_URL !== url.origin || url.pathname.includes("/public")) return;
|
||||||
|
|
||||||
|
event.respondWith(handleImageRequest(req));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleImageRequest(request) {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
|
||||||
|
const cachedResponse = await cache.match(request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// Clone and modify the request with Authorization header
|
||||||
|
const modifiedRequest = new Request(request.url, {
|
||||||
|
method: request.method,
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(request.headers.entries()),
|
||||||
|
Authorization: AUTH_TOKEN,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
mode: 'same-origin',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(modifiedRequest);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
return new Response('Image load failed', { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,19 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('/src/assets/service-worker.js')
|
||||||
|
.then((reg) => {
|
||||||
|
console.log('[SW] Registered:', reg.scope);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[SW] Registration failed:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<StorageProvider>
|
<StorageProvider>
|
||||||
|
|||||||
@@ -1,524 +0,0 @@
|
|||||||
import { FullScreen, theme } from "@nestri/www/ui";
|
|
||||||
import { styled } from "@macaron-css/solid";
|
|
||||||
import { Header } from "@nestri/www/pages/steam/header";
|
|
||||||
import { Modal } from "@nestri/www/ui/modal";
|
|
||||||
import { createEffect, createSignal, Match, onCleanup, Switch } from "solid-js";
|
|
||||||
import { Text } from "@nestri/www/ui/text"
|
|
||||||
import { globalStyle, keyframes } from "@macaron-css/core";
|
|
||||||
import { A } from "@solidjs/router";
|
|
||||||
import Avatar from "@nestri/www/ui/avatar";
|
|
||||||
import { Portal } from "@nestri/www/common/portal";
|
|
||||||
import { QrCodeComponent } from "@nestri/www/components"
|
|
||||||
|
|
||||||
|
|
||||||
const LastPlayedWrapper = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
justifyContent: "center",
|
|
||||||
minHeight: 700,
|
|
||||||
height: "50vw",
|
|
||||||
maxHeight: 800,
|
|
||||||
WebkitBoxPack: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
":after": {
|
|
||||||
content: "",
|
|
||||||
pointerEvents: "none",
|
|
||||||
userSelect: "none",
|
|
||||||
background: `linear-gradient(to bottom,transparent,${theme.color.background.d200})`,
|
|
||||||
width: "100%",
|
|
||||||
left: 0,
|
|
||||||
position: "absolute",
|
|
||||||
bottom: -1,
|
|
||||||
zIndex: 3,
|
|
||||||
height: 320,
|
|
||||||
backdropFilter: "blur(2px)",
|
|
||||||
WebkitBackdropFilter: "blur(1px)",
|
|
||||||
WebkitMaskImage: `linear-gradient(to top,${theme.color.background.d200} 25%,transparent)`,
|
|
||||||
maskImage: `linear-gradient(to top,${theme.color.background.d200} 25%,transparent)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LastPlayedFader = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "3rem",
|
|
||||||
backgroundColor: "rgba(0,0,0,.08)",
|
|
||||||
mixBlendMode: "multiply",
|
|
||||||
backdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
WebkitBackdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
maskImage: "linear-gradient(to top,rgba(0,0,0,.15) 0%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
// background: "linear-gradient(rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 50%, rgba(10, 0, 0, 0.15) 65%, rgba(0, 0, 0, 0.075) 75.5%, rgba(0, 0, 0, 0.035) 82.85%, rgba(0, 0, 0, 0.02) 88%, rgba(0, 0, 0, 0) 100%)",
|
|
||||||
opacity: 0.6,
|
|
||||||
// backdropFilter: "blur(16px)",
|
|
||||||
pointerEvents: "none",
|
|
||||||
zIndex: 1,
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const BackgroundImage = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
backgroundColor: theme.color.background.d200,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
zIndex: 0,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "opacity",
|
|
||||||
backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1203190/ss_97ea9b0b5a6adf3436b31d389cd18d3a647ee4bf.jpg)"
|
|
||||||
// backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/3373660/c4993923f605b608939536b5f2521913850b028a/ss_c4993923f605b608939536b5f2521913850b028a.jpg)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LogoBackgroundImage = styled("div", {
|
|
||||||
base: {
|
|
||||||
position: "fixed",
|
|
||||||
top: "2rem",
|
|
||||||
height: 240,
|
|
||||||
// width: 320,
|
|
||||||
aspectRatio: "16 / 9",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%,0%)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
zIndex: 1,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "opacity",
|
|
||||||
backgroundImage: "url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1203190/logo_2x.png)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Material = styled("div", {
|
|
||||||
base: {
|
|
||||||
backdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
WebkitBackdropFilter: "saturate(160%) blur(60px)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: 6,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
maskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
WebkitMaskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const JoeColor = styled("div", {
|
|
||||||
base: {
|
|
||||||
backgroundColor: "rgba(0,0,0,.08)",
|
|
||||||
mixBlendMode: "multiply",
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: 6,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
maskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)",
|
|
||||||
WebkitMaskImage: "linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,0) 40.82%,rgba(0,0,0,.15) 50%,rgba(0,0,0,.65) 57.14%,rgba(0,0,0,.9) 67.86%,#000 79.08%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GamesContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
zIndex: 10,
|
|
||||||
isolation: "isolate",
|
|
||||||
backgroundColor: theme.color.background.d200,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GamesWrapper = styled("div", {
|
|
||||||
base: {
|
|
||||||
maxWidth: "70vw",
|
|
||||||
width: "100%",
|
|
||||||
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
|
||||||
margin: "0 auto",
|
|
||||||
display: "grid",
|
|
||||||
marginTop: -80,
|
|
||||||
columnGap: 12,
|
|
||||||
rowGap: 10
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameImage = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "460/215",
|
|
||||||
borderRadius: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameSquareImage = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
borderRadius: 10,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
transitionTimingFunction: "cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
transitionProperty: "all",
|
|
||||||
cursor: "pointer",
|
|
||||||
border: `2px solid transparent`,
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)",
|
|
||||||
outline: `2px solid ${theme.color.brand}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const GameImageCapsule = styled("img", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
aspectRatio: "374/448",
|
|
||||||
borderRadius: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamLibrary = styled("div", {
|
|
||||||
base: {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`,
|
|
||||||
padding: "20px 0",
|
|
||||||
margin: "20px auto",
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
// backgroundColor: "red",
|
|
||||||
maxWidth: "70vw",
|
|
||||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
||||||
columnGap: 20,
|
|
||||||
rowGap: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Title = styled("h3", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
fontWeight: theme.font.weight.medium,
|
|
||||||
fontSize: theme.font.size["2xl"],
|
|
||||||
letterSpacing: -0.7,
|
|
||||||
gridColumn: "1/-1",
|
|
||||||
marginBottom: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameTitle = styled("h3", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
fontWeight: theme.font.weight.medium,
|
|
||||||
fontSize: theme.font.size["xl"],
|
|
||||||
letterSpacing: -0.7,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameSubTitle = styled("span", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontWeight: theme.font.weight.regular,
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
letterSpacing: -0.4,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SubTitle = styled("span", {
|
|
||||||
base: {
|
|
||||||
textAlign: "left",
|
|
||||||
fontWeight: theme.font.weight.regular,
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
letterSpacing: -0.4,
|
|
||||||
gridColumn: "1/-1",
|
|
||||||
marginTop: -20,
|
|
||||||
marginBottom: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsList = styled("div", {
|
|
||||||
base: {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`,
|
|
||||||
padding: "20px 0",
|
|
||||||
margin: "20px auto",
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
maxWidth: "70vw",
|
|
||||||
gridTemplateColumns: "repeat(5, minmax(0, 1fr))",
|
|
||||||
columnGap: 12,
|
|
||||||
rowGap: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
minHeight: "calc(100% + 20px)",
|
|
||||||
aspectRatio: "300/380",
|
|
||||||
borderRadius: 15,
|
|
||||||
position: "relative",
|
|
||||||
padding: "35px 17px",
|
|
||||||
border: `1px solid ${theme.color.gray.d500}`,
|
|
||||||
backgroundColor: theme.color.background.d100,
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsSubText = styled("span", {
|
|
||||||
base: {
|
|
||||||
color: theme.color.gray.d900,
|
|
||||||
fontSize: theme.font.size.sm,
|
|
||||||
marginTop: 10,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const FriendsText = styled("h3", {
|
|
||||||
base: {
|
|
||||||
fontSize: theme.font.size["lg"],
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
marginTop: 20,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const FriendsInviteButton = styled("button", {
|
|
||||||
base: {
|
|
||||||
minWidth: 48,
|
|
||||||
borderRadius: 9999,
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "0px 24px",
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
lineHeight: "1.75",
|
|
||||||
marginTop: 20,
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: theme.font.weight.bold,
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
border: `1px solid ${theme.color.gray.d100}`,
|
|
||||||
backgroundColor: theme.color.blue.d700,
|
|
||||||
transition: "all 0.2s cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
padding: "20px 0",
|
|
||||||
width: "100%",
|
|
||||||
minHeight: 72,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
selectors: {
|
|
||||||
"&:not(:last-of-type)": {
|
|
||||||
borderBottom: `1px solid ${theme.color.gray.d400}`
|
|
||||||
},
|
|
||||||
"&:not(:first-of-type)": {
|
|
||||||
borderTop: `1px solid ${theme.color.gray.d400}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameImg = styled("img", {
|
|
||||||
base: {
|
|
||||||
border: "none",
|
|
||||||
outline: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 8
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const SteamGameText = styled("div", {
|
|
||||||
base: {
|
|
||||||
paddingRight: "3em",
|
|
||||||
marginLeft: 30,
|
|
||||||
display: "flex",
|
|
||||||
gap: 8,
|
|
||||||
flexDirection: "column",
|
|
||||||
alignSelf: "center",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const SteamGameBtn = styled("button", {
|
|
||||||
base: {
|
|
||||||
minWidth: 48,
|
|
||||||
borderRadius: 9999,
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "0px 24px",
|
|
||||||
fontSize: theme.font.size["base"],
|
|
||||||
lineHeight: "1.75",
|
|
||||||
// marginTop: 20,
|
|
||||||
// marginRight: 1,
|
|
||||||
margin: "0 1px 0 auto",
|
|
||||||
cursor: "pointer",
|
|
||||||
alignSelf: "center",
|
|
||||||
fontWeight: theme.font.weight.bold,
|
|
||||||
fontFamily: theme.font.family.heading,
|
|
||||||
color: theme.color.blue.d900,
|
|
||||||
border: `1px solid ${theme.color.gray.d100}`,
|
|
||||||
backgroundColor: theme.color.blue.d300,
|
|
||||||
transition: "all 0.2s cubic-bezier(0.4,0,0.2,1)",
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const PortalContainer = styled("div", {
|
|
||||||
base: {
|
|
||||||
zIndex: 4,
|
|
||||||
isolation: "isolate",
|
|
||||||
position: "fixed",
|
|
||||||
bottom: "20vh",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the home page layout for the gaming platform.
|
|
||||||
*
|
|
||||||
* This component wraps its content within a header and a full-screen container,
|
|
||||||
* currently displaying a QR code component. Commented sections indicate planned
|
|
||||||
* enhancements such as game previews, team mate suggestions, and a Steam library.
|
|
||||||
*/
|
|
||||||
export function HomeRoute() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header>
|
|
||||||
<FullScreen >
|
|
||||||
{/* <LastPlayedWrapper>
|
|
||||||
<LastPlayedFader />
|
|
||||||
<LogoBackgroundImage />
|
|
||||||
<BackgroundImage />
|
|
||||||
<Material />
|
|
||||||
<JoeColor />
|
|
||||||
<PortalContainer>
|
|
||||||
<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" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2022/09/22/slime-rancher-2-button-02-1663890048548.jpg" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2023/05/19/cataclismo-button-1684532710313.jpg" />
|
|
||||||
<GameSquareImage draggable={false} alt="Assasin's Creed Shadows" src="https://assets-prd.ignimgs.com/2024/03/27/marvelrivals-1711557092104.jpg" />
|
|
||||||
</GamesWrapper>
|
|
||||||
<FriendsList>
|
|
||||||
<Title>Team Mate Suggestions</Title>
|
|
||||||
<SubTitle>Invite people to join your team and play together</SubTitle>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Wanjohi Ryan" />
|
|
||||||
<FriendsText>Wanjohi Ryan</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Tracy Jones" />
|
|
||||||
<FriendsText>Tracy Jones</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="The65th" />
|
|
||||||
<FriendsText>The65th</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="Menstral" />
|
|
||||||
<FriendsText>Menstral</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
<FriendContainer>
|
|
||||||
<Avatar size={100} name="AstroHot" />
|
|
||||||
<FriendsText>AstroHot</FriendsText>
|
|
||||||
<FriendsSubText>From your Steam Friends</FriendsSubText>
|
|
||||||
<FriendsInviteButton>Invite</FriendsInviteButton>
|
|
||||||
</FriendContainer>
|
|
||||||
</FriendsList>
|
|
||||||
<SteamLibrary>
|
|
||||||
<Title>Your Steam library</Title>
|
|
||||||
<SubTitle>These titles from your Steam Library are fully functional on Nestri</SubTitle>
|
|
||||||
<div>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/05/27/alanwake2-1685200534966.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Alan Wake II</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure, Horror</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2022/09/22/slime-rancher-2-button-02-1663890048548.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Slime Rancher 2</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure, Casual, Indie</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets1.ignimgs.com/2019/07/17/doom-eternal---button-fin-1563400339680.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Doom Eternal</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2022/10/12/dead-space-2023-button-3-1665603079064.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Dead Space</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action, Adventure</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Update</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/01/25/hifirush-1674680068070.jpg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Hi-Fi Rush</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Action</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
<SteamGameContainer>
|
|
||||||
<SteamGameImg draggable={false} src="https://assets-prd.ignimgs.com/2023/08/24/baldursg3-1692894717196.jpeg" />
|
|
||||||
<SteamGameText>
|
|
||||||
<SteamGameTitle>Baldur's Gate 3</SteamGameTitle>
|
|
||||||
<SteamGameSubTitle>Adventure, RPG, Strategy</SteamGameSubTitle>
|
|
||||||
</SteamGameText>
|
|
||||||
<SteamGameBtn>Install</SteamGameBtn>
|
|
||||||
</SteamGameContainer>
|
|
||||||
</div>
|
|
||||||
</SteamLibrary>
|
|
||||||
</GamesContainer>
|
|
||||||
</FullScreen>
|
|
||||||
</Header>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2625420/hero_capsule.jpg?t=1742853642" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2486740/hero_capsule.jpg?t=1742596243" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/870780/hero_capsule.jpg?t=1737800535" />
|
|
||||||
<GameImageCapsule alt="Assasin's Creed Shadows" src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2050650/hero_capsule.jpg?t=1737800535" />
|
|
||||||
*/
|
|
||||||
|
|
||||||
13
packages/www/src/pages/steam/home.tsx
Normal file
13
packages/www/src/pages/steam/home.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { FullScreen } from "@nestri/www/ui"
|
||||||
|
import { Header } from "@nestri/www/pages/steam/header";
|
||||||
|
|
||||||
|
|
||||||
|
export const HomeRoute = () => {
|
||||||
|
return (
|
||||||
|
<Header>
|
||||||
|
<FullScreen>
|
||||||
|
HOEM
|
||||||
|
</FullScreen>
|
||||||
|
</Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { SteamContext } from "@nestri/www/providers/context";
|
|||||||
import { createEffect, createMemo, Match, Switch } from "solid-js";
|
import { createEffect, createMemo, Match, Switch } from "solid-js";
|
||||||
import { NotAllowed, NotFound } from "@nestri/www/pages/not-found";
|
import { NotAllowed, NotFound } from "@nestri/www/pages/not-found";
|
||||||
import { useAccount, useStorage } from "@nestri/www/providers/account";
|
import { useAccount, useStorage } from "@nestri/www/providers/account";
|
||||||
|
import { HomeRoute } from "./home";
|
||||||
|
|
||||||
export const SteamRoute = (
|
export const SteamRoute = (
|
||||||
<Route
|
<Route
|
||||||
@@ -15,14 +16,14 @@ export const SteamRoute = (
|
|||||||
// const storage = useStorage();
|
// const storage = useStorage();
|
||||||
// const openauth = useOpenAuth();
|
// const openauth = useOpenAuth();
|
||||||
|
|
||||||
// const team = createMemo(() =>
|
// const steam = createMemo(() =>
|
||||||
// account.current.teams.find(
|
// account.current.profiles.find(
|
||||||
// (item) => item.id === params.steamID,
|
// (item) => item.id === params.steamID,
|
||||||
// ),
|
// ),
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// createEffect(() => {
|
// createEffect(() => {
|
||||||
// const t = team();
|
// const t = steam();
|
||||||
// if (!t) return;
|
// if (!t) return;
|
||||||
// storage.set("steam", t.id);
|
// storage.set("steam", t.id);
|
||||||
// });
|
// });
|
||||||
@@ -40,22 +41,22 @@ export const SteamRoute = (
|
|||||||
|
|
||||||
// return (
|
// return (
|
||||||
// <Switch>
|
// <Switch>
|
||||||
// <Match when={!team()}>
|
// <Match when={!steam()}>
|
||||||
// {/* TODO: Add a public page for (other) teams */}
|
|
||||||
// <NotAllowed header />
|
// <NotAllowed header />
|
||||||
// </Match>
|
// </Match>
|
||||||
// <Match when={team()}>
|
// <Match when={steam()}>
|
||||||
// <TeamContext.Provider value={() => team()!}>
|
// <SteamContext.Provider value={() => steam()!}>
|
||||||
// <ApiProvider>
|
// <ApiProvider>
|
||||||
// {props.children}
|
// {props.children}
|
||||||
// </ApiProvider>
|
// </ApiProvider>
|
||||||
// </TeamContext.Provider>
|
// </SteamContext.Provider>
|
||||||
// </Match>
|
// </Match>
|
||||||
// </Switch>
|
// </Switch>
|
||||||
// )
|
// )
|
||||||
// }}
|
// }}
|
||||||
>
|
>
|
||||||
<Route path="library" component={LibraryRoute} />
|
<Route path="library" component={LibraryRoute} />
|
||||||
|
<Route path="" component={HomeRoute} />
|
||||||
<Route path="*" component={() => <NotFound header />} />
|
<Route path="*" component={() => <NotFound header />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ const Logo = styled("svg", {
|
|||||||
opacity: "70%",
|
opacity: "70%",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//MaRt@6563
|
|
||||||
export function LibraryRoute() {
|
export function LibraryRoute() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
1
packages/www/src/sst-env.d.ts
vendored
1
packages/www/src/sst-env.d.ts
vendored
@@ -4,6 +4,7 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_URL: string
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_CDN_URL: string
|
||||||
readonly VITE_STAGE: string
|
readonly VITE_STAGE: string
|
||||||
readonly VITE_AUTH_URL: string
|
readonly VITE_AUTH_URL: string
|
||||||
readonly VITE_ZERO_URL: string
|
readonly VITE_ZERO_URL: string
|
||||||
|
|||||||
8
sst-env.d.ts
vendored
8
sst-env.d.ts
vendored
@@ -24,6 +24,10 @@ declare module "sst" {
|
|||||||
"name": string
|
"name": string
|
||||||
"type": "sst.aws.Bus"
|
"type": "sst.aws.Bus"
|
||||||
}
|
}
|
||||||
|
"CDNRouter": {
|
||||||
|
"type": "sst.aws.Router"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
"Database": {
|
"Database": {
|
||||||
"clusterArn": string
|
"clusterArn": string
|
||||||
"database": string
|
"database": string
|
||||||
@@ -124,6 +128,10 @@ declare module "sst" {
|
|||||||
"type": "sst.aws.Service"
|
"type": "sst.aws.Service"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
|
"ZeroStorage": {
|
||||||
|
"name": string
|
||||||
|
"type": "sst.aws.Bucket"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user