Skip to content

Commit

Permalink
Merge pull request #17 from hitochan777/add-attendance
Browse files Browse the repository at this point in the history
Add attendance
  • Loading branch information
hitochan777 committed Jul 26, 2020
2 parents eff5f6d + 50c8aca commit 2010b24
Show file tree
Hide file tree
Showing 9 changed files with 670 additions and 375 deletions.
720 changes: 389 additions & 331 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -15,6 +15,7 @@
"react-dom": "16.13.1",
"react-router-dom": "5.2.0",
"react-scripts": "3.4.1",
"react-semantic-ui-datepickers": "^2.8.0",
"semantic-ui-css": "2.4.1",
"semantic-ui-react": "1.0.0",
"swr": "0.2.3",
Expand Down
130 changes: 130 additions & 0 deletions src/AddForm.tsx
@@ -0,0 +1,130 @@
import React, { useState } from "react";
import { Message, Form, Input } from "semantic-ui-react";
import { AttendanceType } from "./attendance";
import SemanticDatePicker from "react-semantic-ui-datepickers";
import { getTimePart } from "./time";

export type AddForm = {
attendanceType: AttendanceType;
occurredAt: {
date: Date;
time: string;
};
};

type Primitive =
| string
| boolean
| number
| bigint
| symbol
| null
| undefined
| Date;

type FormError<T> = {
[K in keyof T]: T[K] extends Primitive ? string | null : FormError<T[K]>;
};

function validateForm(form: AddForm): FormError<AddForm> {
return { attendanceType: null, occurredAt: { date: null, time: null } };
}

export const useAddForm = (onSubmit: (values: AddForm) => Promise<void>) => {
const now = new Date();
const [form, setForm] = useState<AddForm>({
attendanceType: AttendanceType.Arrive,
occurredAt: {
date: now,
time: getTimePart(now, false),
},
});
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const changeAttendanceType = (_: unknown, data: any) => {
setForm({ ...form, attendanceType: data.value });
};

const handleDateChange = (_: unknown, data: any) => {
setForm({ ...form, occurredAt: { ...form.occurredAt, date: data.value } });
};

const handleTimeChange = (_: unknown, data: any) => {
setForm({ ...form, occurredAt: { ...form.occurredAt, time: data.value } });
};

const handleSubmit = async () => {
const errors = validateForm(form);
if (errors.attendanceType) {
setErrorMessage(errors.attendanceType);
return;
}
if (errors.occurredAt.date) {
setErrorMessage(errors.occurredAt.date);
return;
}
if (errors.occurredAt.time) {
setErrorMessage(errors.occurredAt.time);
return;
}
await onSubmit(form);
};
return {
value: form,
handlers: {
handleDateChange,
handleTimeChange,
handleSubmit,
changeAttendanceType,
},
errorMessage,
};
};

interface Props {
form: {
value: AddForm;
handlers: {
handleDateChange: (_: unknown, data: any) => void;
handleTimeChange: (_: unknown, data: any) => void;
changeAttendanceType: (_: unknown, data: any) => void;
};
errorMessage: string | null;
};
}

export const AddForm: React.FC<Props> = ({
form: { value, handlers, errorMessage },
}) => {
return (
<Form>
<Form.Group inline>
<SemanticDatePicker
value={value.occurredAt.date}
onChange={handlers.handleDateChange}
/>
<Input
value={value.occurredAt.time}
placeholder="hhmmss"
onChange={handlers.handleTimeChange}
></Input>
</Form.Group>
<Form.Group inline>
<Form.Radio
label="Attend"
name="attendanceRadioGroup"
value={AttendanceType.Arrive}
checked={value.attendanceType === AttendanceType.Arrive}
onChange={handlers.changeAttendanceType}
/>
<Form.Radio
label="Leave"
name="attendanceRadioGroup"
value={AttendanceType.Leave}
checked={value.attendanceType === AttendanceType.Leave}
onChange={handlers.changeAttendanceType}
/>
</Form.Group>
{errorMessage && <Message>{errorMessage}</Message>}
</Form>
);
};
5 changes: 4 additions & 1 deletion src/DayHistoryPage.tsx
Expand Up @@ -50,7 +50,10 @@ export const DayHistoryPage: React.FC = () => {
<Table.Cell>
<Button
onClick={() => {
deleteAttendance(data?.userId, attendance);
// eslint-disable-next-line
if (confirm("本当に削除しますか?")) {
deleteAttendance(data?.userId, attendance);
}
}}
disabled={deleting}
>
Expand Down
22 changes: 7 additions & 15 deletions src/LogPage.tsx
Expand Up @@ -3,38 +3,30 @@ import { Button } from "semantic-ui-react";
import { useUser } from "./useUser";
import { Redirect } from "react-router-dom";

async function logAttendance(userId: string, type: "Arrive" | "Leave") {
const data = { type };
await fetch(
`${process.env.REACT_APP_API_ENDPOINT}/api/${userId}/attendance?code=${process.env.REACT_APP_API_KEY}&clientId=attendance-taking-app`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "content-type": "application/json" },
}
);
}
import { useLogAttendance } from "./useAttendances";
import { AttendanceType } from "./attendance";

export function LogPage() {
const { data, loading } = useUser();
const { logAttendance, loading: loggingAttendance } = useLogAttendance();
if (loading) {
return <div>loading...</div>;
}
if (!data) {
return <Redirect to="/signin" />;
}
const handleAttendClick = async () => {
await logAttendance(data.userId, "Arrive");
await logAttendance(data.userId, AttendanceType.Arrive);
};
const handleLeaveClick = async () => {
await logAttendance(data.userId, "Leave");
await logAttendance(data.userId, AttendanceType.Leave);
};
return (
<div>
<Button primary onClick={handleAttendClick}>
<Button primary onClick={handleAttendClick} disabled={loggingAttendance}>
Attend
</Button>
<Button secondary onClick={handleLeaveClick}>
<Button secondary onClick={handleLeaveClick} disabled={loggingAttendance}>
Leave
</Button>
</div>
Expand Down
106 changes: 81 additions & 25 deletions src/Navbar.tsx
@@ -1,7 +1,10 @@
import React from "react";
import React, { useState } from "react";
import { Link, useRouteMatch } from "react-router-dom";
import { Menu } from "semantic-ui-react";
import { Menu, Modal, Button } from "semantic-ui-react";
import { useUser } from "./useUser";
import { AddForm, useAddForm } from "./AddForm";
import { useLogAttendance } from "./useAttendances";
import { extractTimeInfo } from "./time";

type ItemType = "log" | "history";

Expand All @@ -15,33 +18,86 @@ function useActiveItem(): ItemType {
return "log";
}

const useAddFormSubmit = (userId: string | undefined) => {
const { logAttendance, loading } = useLogAttendance();
const submitAddForm = async (values: AddForm) => {
if (!userId) {
return;
}
const date = values.occurredAt.date;
const { hour, minute, second } = extractTimeInfo(values.occurredAt.time);
const occurredAt = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
hour,
minute,
second
);
await logAttendance(userId, values.attendanceType, occurredAt);
};
return { submitAddForm, loading };
};

export const Navbar = () => {
const activeItem = useActiveItem();
const { data } = useUser();
const [isModalOpen, setIsModalOpen] = useState(false);
const { submitAddForm, loading } = useAddFormSubmit(data?.userId);
const addForm = useAddForm(submitAddForm);

const openAddModal = () => {
setIsModalOpen(true);
};

const closeAddModal = () => {
setIsModalOpen(false);
};

return (
<Menu>
<Menu.Item header>Attcy</Menu.Item>
<Link to="/">
<Menu.Item name="log" active={activeItem === "log"} />
</Link>
<Link to="/history">
<Menu.Item name="history" active={activeItem === "history"} />
</Link>
<Menu.Menu position="right">
{data ? (
<>
<Menu.Item>Welcome {data.userDetails}</Menu.Item>
<a href="/.auth/logout">
<Menu.Item name="logout" />
</a>
</>
) : (
<Link to="/signin">
<Menu.Item name="signin"></Menu.Item>
</Link>
)}
</Menu.Menu>
</Menu>
<>
<Menu>
<Menu.Item header>Attcy</Menu.Item>
<Link to="/">
<Menu.Item name="log" active={activeItem === "log"} />
</Link>
<Link to="/history">
<Menu.Item name="history" active={activeItem === "history"} />
</Link>
<Menu.Menu position="right">
{data ? (
<>
<Menu.Item onClick={openAddModal}>Add</Menu.Item>
<Menu.Item>Welcome {data.userDetails}</Menu.Item>
<a href="/.auth/logout">
<Menu.Item name="logout" />
</a>
</>
) : (
<Link to="/signin">
<Menu.Item name="signin"></Menu.Item>
</Link>
)}
</Menu.Menu>
</Menu>
<Modal dimmer="inverted" open={isModalOpen} onClose={closeAddModal}>
<Modal.Header>Add attendance</Modal.Header>
<Modal.Content>
<AddForm form={addForm} />
</Modal.Content>
<Modal.Actions>
<Button color="black" onClick={closeAddModal} disabled={loading}>
Cancel
</Button>
<Button
positive
onClick={addForm.handlers.handleSubmit}
disabled={loading}
>
Add
</Button>
</Modal.Actions>
</Modal>
</>
);
};
1 change: 1 addition & 0 deletions src/index.tsx
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import "react-semantic-ui-datepickers/dist/react-semantic-ui-datepickers.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

Expand Down
18 changes: 16 additions & 2 deletions src/time.ts
@@ -1,3 +1,17 @@
export function getTimePart(date: Date): string {
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
export function getTimePart(date: Date, withColon = true): string {
const hourStr = `${date.getHours()}`;
const minStr = `${date.getMinutes()}`;
const secStr = `${date.getSeconds()}`;
const timeArray = [hourStr, minStr, secStr];
const delimiter = withColon ? ":" : "";
return timeArray.map((timeStr) => timeStr.padStart(2, "0")).join(delimiter);
}

export function extractTimeInfo(
timeStr: string
): { hour: number; minute: number; second: number } {
const hour = +timeStr.substr(0, 2);
const minute = +timeStr.substr(2, 2);
const second = +timeStr.substr(4, 2);
return { hour, minute, second };
}
42 changes: 41 additions & 1 deletion src/useAttendances.ts
@@ -1,6 +1,6 @@
import useSWR, { mutate } from "swr";

import { Attendance } from "./attendance";
import { Attendance, AttendanceType } from "./attendance";
import { fetcher } from "./fetcher";
import { useState } from "react";

Expand Down Expand Up @@ -64,3 +64,43 @@ export function useDeleteAttendance(): {
};
return { deleteAttendance, loading, error };
}

export const useLogAttendance = (): {
logAttendance: (
userId: string,
type: AttendanceType,
occurredAt?: Date
) => Promise<void>;
loading: boolean;
error: Error | null;
} => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

const logAttendance = async (
userId: string,
type: AttendanceType,
occurredAt?: Date
) => {
setLoading(true);
const endpoint = `${process.env.REACT_APP_API_ENDPOINT}/api/${userId}/attendance?code=${process.env.REACT_APP_API_KEY}&clientId=attendance-taking-app`;
try {
const data: { type: string; occurredAt?: number } = {
type: AttendanceType[type],
};
if (occurredAt) {
data.occurredAt = occurredAt.getTime() / 1000;
}
await fetch(endpoint, {
method: "POST",
body: JSON.stringify(data),
headers: { "content-type": "application/json" },
});
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
return { logAttendance, loading, error };
};

0 comments on commit 2010b24

Please sign in to comment.