Skip to content

Commit

Permalink
Merge pull request #21 from JollyGrin/feat/share-connect
Browse files Browse the repository at this point in the history
Feat/share connect
  • Loading branch information
JollyGrin authored Mar 26, 2024
2 parents 0d31c1e + 74595c9 commit cb2c86a
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 266 deletions.
88 changes: 88 additions & 0 deletions components/Connect/SelectedDeck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Flex, Tag, Text } from "@chakra-ui/react";

import { useLocalDeckStorage } from "@/lib/hooks/useLocalStorage";
import Link from "next/link";

export const SelectedDeckContainer = () => {
const { starredDeck } = useLocalDeckStorage();
return (
<Flex
flexDir={"column"}
w="100%"
justifyContent={"center"}
alignItems={"center"}
gap={3}
>
{starredDeck === undefined ? (
<>
<Flex
w="100px"
h="100px"
justifyContent="center"
alignItems="center"
bg="saddlebrown"
boxShadow="inset 0 0 10px black"
borderRadius={5}
flexDir={"column"}
>
<Link href={"/bag"}>
<Text
fontSize="6xl"
color="goldenrod"
cursor="pointer"
_hover={{ color: "gold" }}
>
+
</Text>
</Link>
</Flex>
<Text>
No deck selected! <br /> Star a deck from your bag
</Text>
</>
) : (
<>
<Flex
as={Link}
href="/bag"
w="200px"
h="300px"
bg={starredDeck.deck_data.appearance.highlightColour}
bgImage={starredDeck.deck_data?.appearance?.cardbackUrl}
bgSize="cover"
bgPosition="center"
border={`0.25rem solid ${starredDeck.deck_data.appearance.borderColour}`}
justifyContent="center"
alignItems="center"
boxShadow="0 0 5px rgba(0,0,0,0.5)"
borderRadius={5}
flexDir={"column"}
transition="all 0.35s ease-in-out"
_hover={{
boxShadow: "0 0 10px rgba(0,0,0,0.55)",
filter: "saturate(1.1)",
}}
cursor="pointer"
>
<Text
display={
starredDeck?.deck_data?.appearance?.cardbackUrl
? "none"
: "inline"
}
fontSize="4xl"
fontFamily={"monospace"}
color={starredDeck.deck_data.appearance.borderColour}
_hover={{ color: "gold" }}
>
{starredDeck.name.substring(0, 2)}
</Text>
</Flex>
<Tag p={2} fontFamily={"monospace"} fontSize={"1.1rem"}>
{starredDeck.name}
</Tag>
</>
)}
</Flex>
);
};
193 changes: 193 additions & 0 deletions components/Connect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
Box,
Button,
Flex,
FormLabel,
Grid,
HStack,
Input,
Select,
Spinner,
Tag,
Text,
VStack,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import {
useLocalDeckStorage,
useLocalServerStorage,
} from "@/lib/hooks/useLocalStorage";
import { SettingsModal } from "@/components/Settings/settings.modal";
import { useLoadRouterDeck } from "@/lib/hooks";
import { Navbar } from "@/components/Navbar";
import { SelectedDeckContainer } from "./SelectedDeck";
import { useCreateLobby } from "./useCreateLobby";
import styled from "@emotion/styled";
import { PlusSquareIcon } from "@chakra-ui/icons";
import { useCopyToClipboard } from "@/lib/hooks/useCopyToClipboard";
import { toast } from "react-hot-toast";

export const ConnectPage = () => {
const router = useRouter();
const nameRef = useRef<HTMLInputElement>(null);
const gidRef = useRef<HTMLInputElement>(null);

const [lobby, setLobby] = useState(gidRef?.current?.value ?? "");
const [sharedDeckId, setSharedDeckId] = useState<string | undefined>();

// uses router props to load new decks
useLoadRouterDeck();

const { starredDeck, decks } = useLocalDeckStorage();
const { activeServer, setActiveServer, serverList } = useLocalServerStorage();

// create the lobby and connect to it
const { refetch, loading, setLoading, disclosure } = useCreateLobby({
gidRef,
nameRef,
});

const [_, copy] = useCopyToClipboard();

useEffect(() => {
router.prefetch("/game");
}, [router]);

return (
<Wrapper bgImage="background/choosefighter.png" bgBlendMode="multiply">
<SettingsModal
isOpen={disclosure.isOpen}
onClose={disclosure.onClose}
serverStorage={{ activeServer, setActiveServer, serverList }}
/>
<Box position="absolute" top="0" w="100%" color="brand.primary">
<Navbar />
</Box>
<ConnectContainer backdropFilter="blur(5px)">
<Text fontSize={"2.5rem"} fontWeight={700} fontFamily="SpaceGrotesk">
Connect to a Lobby
</Text>
<SelectedDeckContainer />
<Grid
templateColumns={`1fr ${sharedDeckId === undefined ? "auto" : ""} 1fr`}
w="100%"
gap="0.5rem"
>
{sharedDeckId === undefined && <div />}
<VStack>
<Input
ref={nameRef}
defaultValue={router.query.username as string | undefined}
placeholder="Your name"
bg="white"
/>
<Input
ref={gidRef}
defaultValue={router.query.lobby as string | undefined}
onChange={(e) => {
setLobby(e.target.value);
if (e.target.value === "") setSharedDeckId(undefined);
}}
placeholder="lobby name"
bg="white"
/>
<Button
// If we are loading a new lobby request, freeze the input values.
// isDisabled={(data && isLoading) || starredDeck === undefined}
w="100%"
isDisabled={loading || starredDeck === undefined}
bg="brand.primary"
onClick={() => {
if (!nameRef?.current?.value || !gidRef?.current?.value) {
alert("I need a name and a room name");
return;
}
setLoading(true);
refetch();
}}
>
{loading && <Spinner />}
Connect to Game
</Button>
</VStack>
{sharedDeckId === undefined && (
<PlusSquareIcon
opacity={lobby !== "" ? 1 : 0}
transition="all 0.25s ease-in-out"
onClick={() => setSharedDeckId("")}
fontSize="2rem"
alignSelf="center"
justifySelf="center"
cursor="pointer"
_hover={{
transform: "scale(1.2)",
}}
/>
)}

{sharedDeckId !== undefined && (
<VStack>
<FormLabel mt="7px">Share a link with the deck:</FormLabel>
<Select
bg="white"
onChange={(e) => setSharedDeckId(e.target.value)}
>
{decks?.map((deck) => (
<option key={deck.id} value={deck.version_id ?? deck.id}>
{deck.name}
</option>
))}
</Select>
<Button
w="100%"
onClick={() => {
const baseUrl = "https://unbrewed.xyz/connect";
const sharableUrl = `${baseUrl}?lobby=${gidRef?.current?.value}&deckId=${sharedDeckId}`;
copy(sharableUrl);
toast.success("Copied to clipboard: " + sharableUrl);
}}
>
Copy Sharable Link
</Button>
</VStack>
)}
</Grid>
</ConnectContainer>
<Flex
position="absolute"
bottom="0.5rem"
flexDir={"column"}
alignItems="center"
onClick={disclosure.onOpen}
cursor="pointer"
>
<Text color="ghostwhite">Active GameServer URL:</Text>
<Tag p={2}>{activeServer}</Tag>
</Flex>
</Wrapper>
);
};

const Wrapper = styled(Flex)`
height: 100svh;
background-color: slategray;
background-position: center;
background-size: cover;
justify-content: center;
align-items: center;
position: center;
`;

const ConnectContainer = styled(Flex)`
flex-direction: column;
height: 50%;
width: 95%;
max-width: 600px;
min-height: 600px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 1rem;
padding: 2rem;
justify-content: space-evenly;
align-items: center;
`;
78 changes: 78 additions & 0 deletions components/Connect/useCreateLobby.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import axios from "axios";
import { useLocalServerStorage } from "@/lib/hooks";
import { useQuery } from "@tanstack/react-query";
import { RefObject, useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { useDisclosure } from "@chakra-ui/react";

interface Props {
gidRef: RefObject<HTMLInputElement>;
nameRef: RefObject<HTMLInputElement>;
}

export function useCreateLobby({ gidRef, nameRef }: Props) {
const router = useRouter();
const { activeServer } = useLocalServerStorage();
const serverURL = new URL(activeServer);

// the useQuery loading props not working on repeat visits
const [loading, setLoading] = useState<boolean>(false);

const disclosure = useDisclosure();

const { refetch } = useQuery(
["create-lobby"],
async () => {
if (!gidRef?.current?.value) return;
const createLobbyURL = new URL(
`/lobby/${gidRef.current.value}`,
serverURL,
);
try {
const result = await axios.get(createLobbyURL.toString());
return result.data;
} catch (err) {
console.error(err);
throw err;
}
},
{
// We manually call refetch.
enabled: false,
refetchOnWindowFocus: false,
onSuccess: () => {
// If the lobby was created, we can move to the game state and
// connect to the websocket.
if (!nameRef?.current || !gidRef?.current) return;
router.push(
"/game?name=" +
nameRef.current.value +
"&gid=" +
gidRef.current.value,
);

// HACK: the loading props are borked, so manually setting and resetting state
setTimeout(() => {
setLoading(false);
toast.success(
`Successfully connected to GameServer: \n ${activeServer}`,
);
}, 10000);
},
onError: () => {
// if there's an error connecting, prompt the Gameserver Settings modal so they can input a new server url
setLoading(false);
disclosure.onOpen();
toast.error(`Failed to connect to GameServer: \n ${activeServer}`);
},
},
);

return {
refetch,
loading,
setLoading,
disclosure,
};
}
29 changes: 29 additions & 0 deletions lib/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useState } from "react";

type CopiedValue = string | null;

type CopyFn = (text: string) => Promise<boolean>;

export function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null);

const copy: CopyFn = useCallback(async (text) => {
if (!navigator?.clipboard) {
console.warn("Clipboard not supported");
return false;
}

// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
return true;
} catch (error) {
console.warn("Copy failed", error);
setCopiedText(null);
return false;
}
}, []);

return [copiedText, copy];
}
Loading

0 comments on commit cb2c86a

Please sign in to comment.