Skip to content

Commit

Permalink
Merge pull request #137 from game-node-app/dev
Browse files Browse the repository at this point in the history
Enables PSN in the Importer system
  • Loading branch information
Lamarcke authored Jan 6, 2025
2 parents bc2afd3 + e0e70a6 commit b9d9d5f
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 201 deletions.
2 changes: 1 addition & 1 deletion src/components/game/figure/GameFigureImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const GameFigureImage = ({
onClick={onClick}
{...linkProps}
>
<AspectRatio ratio={264 / 354} pos="relative" h={"100%"} w={"auto"}>
<AspectRatio ratio={264 / 354} pos="relative" w={"auto"}>
<Image
radius={"sm"}
src={sizedCoverUrl ?? "/img/game_placeholder.jpeg"}
Expand Down
1 change: 1 addition & 0 deletions src/components/game/view/select/GameSelectViewContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface Props extends PropsWithChildren<SimpleGridProps & SelectedProps> {
* @param checkIsSelected
* @param onSelected
* @param excludeItemsInLibrary
* @param onExcludedItemClick
* @param others
* @constructor
*/
Expand Down
28 changes: 17 additions & 11 deletions src/components/game/view/select/GameSelectViewFigure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react";
import GameFigureImage, {
IGameFigureProps,
} from "@/components/game/figure/GameFigureImage";
import { Overlay, Stack, Text, ThemeIcon } from "@mantine/core";
import { Box, Center, Overlay, Stack, Text, ThemeIcon } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
import { useOwnCollectionEntryForGameId } from "@/components/collection/collection-entry/hooks/useOwnCollectionEntryForGameId";
Expand Down Expand Up @@ -77,11 +77,13 @@ const GameSelectViewFigure = ({
backgroundOpacity={0.85}
className={"z-10"}
/>
<IconCircleCheckFilled
<Center
className={
"absolute left-1/2 right-1/2 -translate-x-1/2 -translate-y-1/2 top-1/2 bottom-1/2 w-8 h-8 z-20 text-brand-5"
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-20"
}
/>
>
<IconCircleCheckFilled className={"text-brand-5"} />
</Center>
</>
)}
{isExcluded && (
Expand All @@ -91,16 +93,20 @@ const GameSelectViewFigure = ({
backgroundOpacity={0.85}
className={"z-10"}
/>
<div
<Center
className={
"absolute flex flex-col left-1/2 right-1/2 -translate-x-1/2 -translate-y-1/2 top-1/2 bottom-1/2 z-20"
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-20 items-center"
}
>
<IconCircleCheckFilled
className={"relative w-8 h-8 z-20 text-brand-5"}
/>
<Text className={"mt-2"}>In your library</Text>
</div>
<Stack className={"items-center gap-0.5"}>
<IconCircleCheckFilled
className={"w-8 h-8 z-20 text-brand-5"}
/>
<Text className={"text-center"}>
In your library
</Text>
</Stack>
</Center>
</>
)}
</GameFigureImage>
Expand Down
10 changes: 6 additions & 4 deletions src/components/importer/view/ImporterItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useMemo } from "react";
import { UserConnection } from "@/wrapper/server";
import { UserConnectionDto } from "@/wrapper/server";
import { Button, Image, Paper, Stack, Title } from "@mantine/core";
import { getServerStoredIcon } from "@/util/getServerStoredImages";
import Link from "next/link";

interface Props {
connection: UserConnection;
connection: UserConnectionDto;
}

const connectionTypeToName = (type: UserConnection.type) => {
const connectionTypeToName = (type: UserConnectionDto.type) => {
switch (type) {
case UserConnection.type.STEAM:
case UserConnectionDto.type.STEAM:
return "Steam";
case UserConnectionDto.type.PSN:
return "Playstation Network";
default:
return "Name not available";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ const ImporterWatchAggregatedNotification = ({
h={38}
/>
<Text>
We've found {notificationQuery.data.games.length} new games
ready to be imported from your {sourceName} connection.
We've found {notificationQuery.data?.games?.length} new
games ready to be imported from your {sourceName}{" "}
connection.
</Text>
</Group>
</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from "react";
import { UserConnection } from "@/wrapper/server";
import type = UserConnection.type;
import { UserConnectionDto } from "@/wrapper/server";
import type = UserConnectionDto.type;
import { useOwnUserConnectionByType } from "@/components/connections/hooks/useOwnUserConnectionByType";
import { useDisclosure } from "@mantine/hooks";
import { Group, Image, Paper, Stack, Switch, Text, Title } from "@mantine/core";
Expand Down Expand Up @@ -31,7 +31,7 @@ const PreferencesConnectionItem = ({ type }: Props) => {
onClose={modalUtils.close}
/>
<Image
alt={"Steam icon"}
alt={"Connection icon"}
src={getServerStoredIcon(type.valueOf())}
w={38}
h={38}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import React, { useMemo } from "react";
import { BaseModalProps } from "@/util/types/modal-props";
import { UserConnection } from "@/wrapper/server";
import { UserConnectionDto } from "@/wrapper/server";
import { Modal } from "@mantine/core";
import PreferencesConnectionSteamForm from "@/components/preferences/handlers/connections/steam/PreferencesConnectionSteamForm";
import PreferencesConnectionSetup from "@/components/preferences/handlers/connections/PreferencesConnectionSetup";

interface Props extends BaseModalProps {
type: UserConnection.type;
type: UserConnectionDto.type;
}

const PreferencesConnectionModal = ({ opened, onClose, type }: Props) => {
const renderedConnectionForm = useMemo(() => {
switch (type) {
case UserConnection.type.STEAM:
return <PreferencesConnectionSteamForm onClose={onClose} />;
default:
return null;
}
}, [onClose, type]);
return (
<Modal title={"Set up connection"} onClose={onClose} opened={opened}>
<Modal.Body>{renderedConnectionForm}</Modal.Body>
<PreferencesConnectionSetup type={type} onClose={onClose} />
</Modal>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, { useEffect, useMemo } from "react";
import { BaseModalChildrenProps } from "@/util/types/modal-props";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { ConnectionsService, UserConnectionDto } from "@/wrapper/server";
import { useOwnUserConnectionByType } from "@/components/connections/hooks/useOwnUserConnectionByType";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { getErrorMessage } from "@/util/getErrorMessage";
import { getCapitalizedText } from "@/util/getCapitalizedText";
import CenteredErrorMessage from "@/components/general/CenteredErrorMessage";
import { Button, Stack, Switch, Text, TextInput } from "@mantine/core";
import { useAvailableConnections } from "@/components/connections/hooks/useAvailableConnections";

const ConnectionSetupFormSchema = z.object({
userIdentifier: z.string().min(1, "A username must be provided."),
isImporterEnabled: z.boolean().default(true),
});

type ConnectionSetupFormValues = z.infer<typeof ConnectionSetupFormSchema>;

export interface Props extends BaseModalChildrenProps {
type: UserConnectionDto.type;
}

const PreferencesConnectionSetup = ({ type, onClose }: Props) => {
const {
register,
watch,
setValue,
handleSubmit,
formState: { errors },
} = useForm<ConnectionSetupFormValues>({
mode: "onBlur",
defaultValues: {
isImporterEnabled: true,
},
resolver: zodResolver(ConnectionSetupFormSchema),
});

const userConnection = useOwnUserConnectionByType(type);

const availableConnections = useAvailableConnections();

const queryClient = useQueryClient();

const connectionCreateMutation = useMutation({
mutationFn: async (data: ConnectionSetupFormValues) => {
await ConnectionsService.connectionsControllerCreateOrUpdateV1({
type: type,
userIdentifier: data.userIdentifier,
isImporterEnabled: data.isImporterEnabled,
});
},
onSuccess: () => {
notifications.show({
color: "green",
message: `Successfully set up ${getCapitalizedText(type)} connection!`,
});
if (onClose) {
onClose();
}
},
onError: (err) => {
notifications.show({
color: "red",
message: getErrorMessage(err),
});
},
onSettled: () => {
queryClient.resetQueries({
queryKey: ["connections", "own"],
});
},
});

const connectionDeleteMutation = useMutation({
mutationFn: async () => {
if (userConnection.data == undefined) {
return;
}

return ConnectionsService.connectionsControllerDeleteV1(
userConnection.data.id,
);
},
onSuccess: () => {
notifications.show({
color: "green",
message: `Successfully removed ${getCapitalizedText(type)} connection!`,
});
if (onClose) {
onClose();
}
},
onSettled: () => {
queryClient.resetQueries({
queryKey: ["connections"],
});
},
});

const isImporterViable = useMemo(() => {
if (availableConnections.data != undefined) {
return availableConnections.data.some((connection) => {
return connection.type === type && connection.isImporterViable;
});
}

return false;
}, [availableConnections.data, type]);

const identifierInfo = useMemo((): {
label: string;
description: string;
} => {
switch (type) {
case UserConnectionDto.type.STEAM:
return {
label: "Your public Steam profile URL",
description:
"e.g.: https://steamcommunity.com/id/your-username/",
};
case UserConnectionDto.type.PSN:
return {
label: "Your PSN online id",
description: "Usually, it's your username.",
};
}
}, [type]);

/**
* Effect to synchronize form state with user connection info.
*/
useEffect(() => {
if (userConnection.data) {
setValue(
"isImporterEnabled",
userConnection.data.isImporterEnabled,
);
}
}, [setValue, userConnection.data]);

return (
<form
className={"w-full h-full"}
onSubmit={handleSubmit((data) => {
connectionCreateMutation.mutate(data);
})}
>
<Stack className={"w-full h-full"}>
{connectionCreateMutation.error && (
<CenteredErrorMessage
message={getErrorMessage(
connectionCreateMutation.error,
)}
/>
)}
{connectionDeleteMutation.error && (
<CenteredErrorMessage
message={getErrorMessage(
connectionDeleteMutation.error,
)}
/>
)}
<TextInput
error={errors.userIdentifier?.message}
label={identifierInfo.label}
description={identifierInfo.description}
defaultValue={userConnection.data?.sourceUsername}
{...register("userIdentifier")}
/>
{isImporterViable && (
<Stack>
<Switch
error={errors.isImporterEnabled?.message}
label={"Allow importing"}
labelPosition={"left"}
defaultChecked={
userConnection.data
? userConnection.data.isImporterEnabled
: true
}
description={
"If this connection can be used by the Importer system to import games."
}
{...register("isImporterEnabled")}
/>
</Stack>
)}

<Button
type={"submit"}
loading={connectionCreateMutation.isPending}
>
Submit
</Button>
{userConnection.data != undefined && (
<Button
color={"blue"}
type={"button"}
loading={connectionDeleteMutation.isPending}
onClick={() => {
connectionDeleteMutation.mutate();
}}
>
Disconnect
</Button>
)}
</Stack>
</form>
);
};

export default PreferencesConnectionSetup;
Loading

0 comments on commit b9d9d5f

Please sign in to comment.