diff --git a/apps/cyberstorm-remix/app/entry.server.tsx b/apps/cyberstorm-remix/app/entry.server.tsx
index 79099f4a0..5c47abfc2 100644
--- a/apps/cyberstorm-remix/app/entry.server.tsx
+++ b/apps/cyberstorm-remix/app/entry.server.tsx
@@ -26,7 +26,7 @@ export default function handleRequest(
remixContext: EntryContext,
loadContext: AppLoadContext
) {
- let prohibitOutOfOrderStreaming =
+ const prohibitOutOfOrderStreaming =
isBotRequest(request.headers.get("user-agent")) || remixContext.isSpaMode;
return prohibitOutOfOrderStreaming
diff --git a/apps/cyberstorm-remix/app/root.tsx b/apps/cyberstorm-remix/app/root.tsx
index 105587a17..6da9eef07 100644
--- a/apps/cyberstorm-remix/app/root.tsx
+++ b/apps/cyberstorm-remix/app/root.tsx
@@ -10,6 +10,7 @@ import {
ScrollRestoration,
isRouteErrorResponse,
useLoaderData,
+ useLocation,
useRouteError,
} from "@remix-run/react";
// import { LinksFunction } from "@remix-run/react/dist/routeModules";
@@ -105,6 +106,9 @@ export async function clientLoader() {
}
// REMIX TODO: Do we want to force a hydration at the root level?
+// Answer: Yes and no, without manual hydrating the header will not update
+// to have the user stuff, since the initial data from server loader will be used
+// until data changes or updates
// clientLoader.hydrate = true;
const adContainerIds = ["right-column-1", "right-column-2", "right-column-3"];
@@ -119,6 +123,9 @@ function Root() {
currentUser: CurrentUser;
} = JSON.parse(JSON.stringify(loaderOutput));
+ const location = useLocation();
+ const shouldShowAds = !location.pathname.startsWith("/teams");
+
return (
@@ -149,9 +156,11 @@ function Root() {
- {adContainerIds.map((cid, k_i) => (
-
- ))}
+ {shouldShowAds
+ ? adContainerIds.map((cid, k_i) => (
+
+ ))
+ : null}
@@ -162,7 +171,7 @@ function Root() {
-
+ {shouldShowAds ? : null}
);
@@ -264,9 +273,9 @@ function AdsInit() {
const nitroAds = typeof window !== "undefined" ? window.nitroAds : undefined;
useEffect(() => {
- if (nitroAds !== undefined) {
+ if (nitroAds !== undefined && nitroAds.createAd !== undefined) {
adContainerIds.forEach((cid) => {
- if (nitroAds !== undefined) {
+ if (nitroAds !== undefined && nitroAds.createAd !== undefined) {
nitroAds.createAd(cid, {
demo: false,
format: "display",
diff --git a/apps/cyberstorm-remix/app/settings/teams/Teams.tsx b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx
new file mode 100644
index 000000000..cfe3d284c
--- /dev/null
+++ b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx
@@ -0,0 +1,142 @@
+import type { MetaFunction } from "@remix-run/node";
+import { useLoaderData, useRevalidator } from "@remix-run/react";
+import {
+ BreadCrumbs,
+ Button,
+ CyberstormLink,
+ Dialog,
+ PageHeader,
+ SettingItem,
+ Table,
+} from "@thunderstore/cyberstorm";
+import { faPlus } from "@fortawesome/pro-solid-svg-icons";
+import styles from "./TeamsLayout.module.css";
+import rootStyles from "../../RootLayout.module.css";
+import { getDapper } from "cyberstorm/dapper/sessionUtils";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useEffect, useState } from "react";
+import { CreateTeamForm } from "@thunderstore/cyberstorm-forms";
+import { currentUserSchema } from "@thunderstore/dapper-ts";
+
+export const meta: MetaFunction = ({
+ data,
+}) => {
+ return [
+ { title: `Teams of ${data?.currentUser.username}` },
+ { name: "description", content: `Teams of ${data?.currentUser.username}` },
+ ];
+};
+
+export async function loader() {
+ const dapper = await getDapper();
+ const currentUser = await dapper.getCurrentUser();
+ return {
+ currentUser: currentUser as typeof currentUserSchema._type,
+ };
+}
+
+export async function clientLoader() {
+ const dapper = await getDapper(true);
+ const currentUser = await dapper.getCurrentUser();
+ if (!currentUser.username) {
+ throw new Response("Not logged in.", { status: 401 });
+ } else {
+ return {
+ currentUser: currentUser as typeof currentUserSchema._type,
+ };
+ }
+}
+
+clientLoader.hydrate = true;
+
+// REMIX TODO: The "Create team" form is missing data refetching after creating a team
+// Copy the stuff from package liking
+export default function Teams() {
+ const { currentUser } = useLoaderData();
+ const [dialogOpen, setOpenDialog] = useState(false);
+ const revalidator = useRevalidator();
+ const [isRefetching, setIsRefetching] = useState(false);
+
+ useEffect(() => {
+ setIsRefetching(false);
+ }, [currentUser]);
+
+ // REMIX TODO: Move current user to stand-alone loader and revalidate only currentUser
+ async function createTeamRevalidate() {
+ if (!isRefetching) {
+ setIsRefetching(true);
+ revalidator.revalidate();
+ }
+ }
+
+ return (
+ <>
+
+ Teams
+
+
+
+
+ Create team
+
+
+
+
+ }
+ >
+
+
+ }
+ content={
+
+
[
+ {
+ value: (
+
+ {team.name}
+
+ ),
+ sortValue: team.name,
+ },
+ {
+ value: team.role,
+ sortValue: team.role,
+ },
+ {
+ value: team.member_count,
+ sortValue: team.member_count,
+ },
+ ])}
+ variant="itemList"
+ />
+
+ }
+ />
+
+ >
+ );
+}
diff --git a/apps/cyberstorm-remix/app/settings/teams/TeamsLayout.module.css b/apps/cyberstorm-remix/app/settings/teams/TeamsLayout.module.css
new file mode 100644
index 000000000..2769de3e8
--- /dev/null
+++ b/apps/cyberstorm-remix/app/settings/teams/TeamsLayout.module.css
@@ -0,0 +1,18 @@
+.contentWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap--16);
+}
+
+.boldText {
+ font-weight: var(--font-weight-bold);
+}
+
+/* TODO: Text in header row and content rows are not aligned */
+
+/* TODO: why do we have this hover color, and should it apply to hover
+ on the whole row, not just one column?
+*/
+.nameColumn:hover {
+ color: rgb(57 233 170);
+}
diff --git a/apps/cyberstorm-remix/cyberstorm/dapper/sessionUtils.ts b/apps/cyberstorm-remix/cyberstorm/dapper/sessionUtils.ts
index 321c7606a..b4e046927 100644
--- a/apps/cyberstorm-remix/cyberstorm/dapper/sessionUtils.ts
+++ b/apps/cyberstorm-remix/cyberstorm/dapper/sessionUtils.ts
@@ -6,10 +6,17 @@ export async function getDapper(isClient = false) {
const dapper = window.Dapper;
let shouldRemakeDapper = false;
+
+ const allCookies = new URLSearchParams(
+ window.document.cookie.replaceAll("&", "%26").replaceAll("; ", "&")
+ );
+ const cookie = allCookies.get("sessionid");
+ const csrftoken = allCookies.get("csrftoken");
+
const newConfig: RequestConfig = {
apiHost: window.ENV.PUBLIC_API_URL,
- sessionId: undefined,
- csrfToken: undefined,
+ sessionId: cookie ?? undefined,
+ csrfToken: csrftoken ?? undefined,
};
if (dapper) {
@@ -20,12 +27,6 @@ export async function getDapper(isClient = false) {
// shouldRemakeDapper = true;
// }
- const allCookies = new URLSearchParams(
- window.document.cookie.replaceAll("&", "%26").replaceAll("; ", "&")
- );
- const cookie = allCookies.get("sessionid");
- const csrftoken = allCookies.get("csrftoken");
-
// sessionId is old
if (
dapper.config.sessionId &&
diff --git a/apps/cyberstorm-remix/cyberstorm/navigation/UserActions.tsx b/apps/cyberstorm-remix/cyberstorm/navigation/UserActions.tsx
index 75a88f086..821e972ce 100644
--- a/apps/cyberstorm-remix/cyberstorm/navigation/UserActions.tsx
+++ b/apps/cyberstorm-remix/cyberstorm/navigation/UserActions.tsx
@@ -1,9 +1,9 @@
-import { faSignOut } from "@fortawesome/pro-solid-svg-icons";
+import { faSignOut, faUsers } from "@fortawesome/pro-solid-svg-icons";
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./Navigation.module.css";
-import { Avatar, Icon } from "@thunderstore/cyberstorm";
+import { Avatar, CyberstormLink, Icon } from "@thunderstore/cyberstorm";
import { DropDownLink } from "@thunderstore/cyberstorm/src/components/DropDown/DropDownLink";
import {
OverwolfLogo,
@@ -28,6 +28,16 @@ export function UserActions(props: { user: CurrentUser }) {
,
+
+ }
+ label="Teams"
+ />
+ ,
,
+
+ }
+ label="Teams"
+ />
+ }
+ />
+ ,
+
+ ,
+
void;
+ updateTrigger: () => Promise;
}) {
const { onSubmitSuccess, onSubmitError } = useFormToaster({
successMessage: "Team created",
@@ -22,6 +23,7 @@ export function CreateTeamForm(props: {
return (
{
+ props.updateTrigger();
onSubmitSuccess();
props.dialogOnChange(false);
}}
diff --git a/packages/dapper-ts/src/index.ts b/packages/dapper-ts/src/index.ts
index 3a882b43a..7133552ed 100644
--- a/packages/dapper-ts/src/index.ts
+++ b/packages/dapper-ts/src/index.ts
@@ -6,6 +6,7 @@ import { getCommunities, getCommunity } from "./methods/communities";
import { getCommunityFilters } from "./methods/communityFilters";
import { getCurrentUser, emptyUser } from "./methods/currentUser";
export const getEmptyUser = emptyUser;
+export { currentUserSchema } from "./methods/currentUser";
import {
getPackageChangelog,
getPackageReadme,
diff --git a/packages/dapper-ts/src/methods/currentUser.ts b/packages/dapper-ts/src/methods/currentUser.ts
index e8b2d1c05..f0233edfc 100644
--- a/packages/dapper-ts/src/methods/currentUser.ts
+++ b/packages/dapper-ts/src/methods/currentUser.ts
@@ -16,6 +16,18 @@ const teamSchema = z.object({
member_count: z.number().int().gte(1),
});
+export const currentUserSchema = z.object({
+ username: z.string().nonempty(),
+ capabilities: z.string().array(),
+ connections: oAuthConnectionSchema.array(),
+ rated_packages: z.string().array(),
+ rated_packages_cyberstorm: z.string().array(),
+ subscription: z.object({
+ expires: z.string().datetime().nullable(),
+ }),
+ teams: teamSchema.array(),
+});
+
const schema = z.object({
username: z.string().nonempty(),
capabilities: z.string().array(),