Skip to content

Commit

Permalink
Merge branch 'develop' into feature/149
Browse files Browse the repository at this point in the history
  • Loading branch information
Ran350 committed Oct 22, 2022
2 parents 8511542 + 2dead03 commit 3f4e704
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 68 deletions.
72 changes: 54 additions & 18 deletions app/src/main/pointer/src/components/JoinRoomForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import {
FormHelperText,
Button,
FormErrorMessage,
useToast,
PinInput,
PinInputField,
Flex,
} from "@chakra-ui/react";
import { AxiosError } from "axios";
import { FC } from "react";
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";

import { roomApi } from "@/api";
import { AuthData } from "@/types/AuthData";
Expand All @@ -28,10 +33,13 @@ type FormValues = {
export const JoinRoomForm: FC<Props> = ({ onSubmit: onSubmitProps }) => {
const url = new URL(location.href);

const toast = useToast();

const {
handleSubmit,
register,
formState: { errors, isSubmitting },
control,
} = useForm<FormValues>({
defaultValues: {
userName: localStorage.getItem(localStorageKey) ?? "",
Expand All @@ -41,17 +49,35 @@ export const JoinRoomForm: FC<Props> = ({ onSubmit: onSubmitProps }) => {
});

const onSubmit = async (values: FormValues) => {
console.log(values);
const { data } = await roomApi.joinRoom(values.roomId, {
passcode: values.passcode,
name: values.userName,
});
localStorage.setItem(localStorageKey, values.userName);
onSubmitProps({
...data,
userName: values.userName,
roomId: values.roomId,
});
try {
const { data } = await roomApi.joinRoom(values.roomId, {
passcode: values.passcode,
name: values.userName,
});
localStorage.setItem(localStorageKey, values.userName);
onSubmitProps({
...data,
userName: values.userName,
roomId: values.roomId,
});

toast({
title: "ログインに成功しました。",
status: "success",
duration: 3000,
isClosable: true,
});
} catch (error) {
if (error instanceof AxiosError) {
toast({
title: "ログインに失敗しました。",
description: error.response?.data.message,
status: "error",
duration: 3000,
isClosable: true,
});
}
}
};

return (
Expand Down Expand Up @@ -83,12 +109,22 @@ export const JoinRoomForm: FC<Props> = ({ onSubmit: onSubmitProps }) => {
</FormControl>
<FormControl isInvalid={errors.passcode !== undefined}>
<FormLabel>パスコード</FormLabel>
<Input
type="text"
{...register("passcode", {
required: "パスコードは必須です",
})}
/>
<Flex justify="center" gap="4">
<Controller
name="passcode"
control={control}
render={({ field: { onChange, value } }) => (
<PinInput type="number" onChange={onChange} value={value}>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
)}
/>
</Flex>
<FormErrorMessage>{errors.passcode?.message}</FormErrorMessage>
</FormControl>
<Button isLoading={isSubmitting} type="submit">
Expand Down
206 changes: 157 additions & 49 deletions app/src/main/pointer/src/components/Timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,149 @@ import {
InputLeftAddon,
InputRightAddon,
Heading,
useToast,
} from "@chakra-ui/react";
import { Cog6ToothIcon } from "@heroicons/react/24/solid";
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import { useForm } from "react-hook-form";

import { stompClient } from "@/stomp";
import { AuthData } from "@/types/AuthData";

type Props = {
authData: AuthData;
};

type FormValues = {
minutes: string;
seconds: string;
};

export const Timer: FC<Props> = ({ authData }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [state, setState] = useState<"READY" | "RUNNING">("READY");
const [finishTimestamp, setFinishedTimestamp] = useState<number | null>(null);
const [noticeTime, setNoticeTime] = useState<string>("1");

const toast = useToast();

const completeNoticeTime = () => {
if (noticeTime === "") {
setNoticeTime("0");
}
onClose();
};

const { register, handleSubmit, setValue } = useForm<FormValues>();

const onStartStop = (values: FormValues) => {
console.log(values);
switch (state) {
case "READY": {
stompClient.publish({
destination: `/app/rooms/${authData.roomId}/timer/start`,
body: JSON.stringify({
value: Number(values.minutes) * 60 + Number(values.seconds),
}),
});
return;
}
case "RUNNING": {
stompClient.publish({
destination: `/app/rooms/${authData.roomId}/timer/stop`,
});
return;
}
}
};

useEffect(() => {
stompClient.subscribe(
`/topic/rooms/${authData.roomId}/timer`,
(message) => {
console.log(message);
const { status, value, finishAt } = JSON.parse(message.body) as {
status: 0 | 1;
value: number;
finishAt: string;
};
switch (status) {
case 0: {
const minutes = Math.floor(value / 60);
const seconds = value % 60;
setValue("minutes", minutes.toString());
setValue("seconds", seconds.toString());
setState("READY");
return;
}
case 1: {
setFinishedTimestamp(
performance.now() + new Date(finishAt).valueOf() - Date.now()
);
setState("RUNNING");
return;
}
}
}
);
}, [authData.roomId, setValue]);

useEffect(() => {
if (finishTimestamp === null) {
return;
}

let requestId: number | null;

const tick = (now: number) => {
const timeStamp = finishTimestamp - now;

if (timeStamp <= 0) {
setValue("minutes", "0");
setValue("seconds", "0");
setState("READY");
toast({
title: "タイマーが終了しました",
description: "お疲れ様でした",
status: "success",
duration: 9000,
isClosable: true,
});
return;
}

const seconds = Math.floor((timeStamp / 1000) % 60);
const minutes = Math.floor((timeStamp / 1000 / 60) % 60);
setValue("minutes", minutes.toString());
setValue("seconds", seconds.toString());

requestId = requestAnimationFrame(tick);
};

if (state === "RUNNING") {
requestId = requestAnimationFrame(tick);
}

return () => {
if (requestId) {
cancelAnimationFrame(requestId);
}
};
}, [finishTimestamp, setValue, state]);

return (
<VStack gap={2}>
<VStack gap={4}>
<VStack gap={4} as="form" onSubmit={handleSubmit(onStartStop)}>
<Flex justify="center" align="center">
<NumberInput defaultValue={5} min={0} size="lg" flexGrow={1}>
<NumberInputField textAlign="right" fontSize="5xl" height={24} />
<NumberInputField
textAlign="right"
fontSize="5xl"
height={24}
{...register("minutes", {
required: true,
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
Expand All @@ -67,66 +182,59 @@ export const Timer: FC<Props> = ({ authData }) => {
/>
</Icon>
<NumberInput defaultValue={0} min={0} max={59} size="lg" flexGrow={1}>
<NumberInputField textAlign="right" fontSize="5xl" height={24} />
<NumberInputField
textAlign="right"
fontSize="5xl"
height={24}
{...register("seconds", {
required: true,
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Flex>
<Flex gap={8}>
<Button
width={24}
disabled={state === "READY"}
onClick={() => setState("READY")}
>
リセット
</Button>
<Button
width={24}
onClick={() =>
setState((oldState) =>
oldState === "READY" ? "RUNNING" : "READY"
)
}
>
<Button width={24} type="submit">
{state === "READY" ? "スタート" : "ストップ"}
</Button>
<Button onClick={onOpen}>
<Icon as={Cog6ToothIcon} />
</Button>
<Modal isOpen={isOpen} onClose={completeNoticeTime}>
<ModalOverlay />
<ModalContent>
<ModalHeader>残り時間通知</ModalHeader>
<ModalCloseButton />
<ModalBody>
<InputGroup>
<InputLeftAddon>残り</InputLeftAddon>
<NumberInput
min={0}
value={noticeTime}
onChange={(value) => setNoticeTime(value)}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<InputRightAddon></InputRightAddon>
</InputGroup>
</ModalBody>

<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={completeNoticeTime}>
完了
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Flex>
</VStack>
<Modal isOpen={isOpen} onClose={completeNoticeTime}>
<ModalOverlay />
<ModalContent>
<ModalHeader>残り時間通知</ModalHeader>
<ModalCloseButton />
<ModalBody>
<InputGroup>
<InputLeftAddon>残り</InputLeftAddon>
<NumberInput
min={0}
value={noticeTime}
onChange={(value) => setNoticeTime(value)}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<InputRightAddon></InputRightAddon>
</InputGroup>
</ModalBody>

<ModalFooter>
<Button colorScheme="blue" mr={3} onClick={completeNoticeTime}>
完了
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
);
};
3 changes: 2 additions & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"productName": "smartpointer",
"description": "手元のスマホをリモコン化する、共同プレゼンテーション支援ツール",
"scripts": {
"dev": "NODE_ENV=development vite",
"dev": "cross-env NODE_ENV=development vite",
"build": "tsc && vite build",
"codegen": "openapi-generator-cli generate -g typescript-axios -i http://localhost:8080/v3/api-docs/Public%20API -o ./electron/main/generated/http-client",
"lint": "eslint src electron",
Expand Down Expand Up @@ -53,6 +53,7 @@
"@typescript-eslint/eslint-plugin": "5.40.1",
"@typescript-eslint/parser": "5.40.1",
"@vitejs/plugin-react": "2.1.0",
"cross-env": "^7.0.3",
"electron": "^21.1.1",
"electron-builder": "^23.3.3",
"electron-reload": "^2.0.0-alpha.1",
Expand Down
7 changes: 7 additions & 0 deletions desktop/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2926,6 +2926,13 @@ crc@^3.8.0:
dependencies:
buffer "^5.1.0"

cross-env@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
dependencies:
cross-spawn "^7.0.1"

cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
Expand Down

0 comments on commit 3f4e704

Please sign in to comment.