Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(web): date time field input #767

Merged
merged 12 commits into from
Oct 27, 2023
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"lodash-es": "4.17.21",
"lru-cache": "8.0.4",
"mini-svg-data-uri": "1.4.4",
"moment-timezone": "0.5.43",
"parse-domain": "7.0.1",
"quickjs-emscripten": "0.23.0",
"quickjs-emscripten-sync": "1.5.2",
Expand Down
3 changes: 3 additions & 0 deletions web/src/beta/components/Icon/Icons/Clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/src/beta/components/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import ZoomToLayer from "./Icons/zoomToLayer.svg";
import LayerStyleIcon from "./Icons/layerStyle.svg";
import AddLayerStyleButtonIcon from "./Icons/addLayerStyleButton.svg";
import LayerInspector from "./Icons/layerInspector.svg";
import Clock from "./Icons/Clock.svg";

// MSIC
import CheckCircle from "./Icons/checkCircle.svg";
Expand Down Expand Up @@ -123,6 +124,7 @@ export default {
text: InfoText,
html: InfoHTML,
video: InfoVideo,
clock: Clock,
location: InfoLocation,
photooverlay: PrimPhotoOverlay,
arrowUpDown: ArrowUpDown,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Camera } from "@reearth/beta/utils/value";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import PanelCommon from "../PanelCommon";
import PanelCommon from "../../common/PanelCommon";
import type { RowType } from "../types";

type Props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Camera } from "@reearth/beta/utils/value";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import PanelCommon from "../PanelCommon";
import PanelCommon from "../../common/PanelCommon";

import useHooks from "./hooks";

Expand Down
89 changes: 89 additions & 0 deletions web/src/beta/components/fields/DateTimeField/EditPanel/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import moment from "moment-timezone";
import { useCallback, useEffect, useMemo, useState } from "react";

import { getUniqueTimezones } from "@reearth/beta/utils/moment-timezone";

type Props = {
value?: string;
onChange?: (value?: string | undefined) => void;
setDateTime?: (value?: string | undefined) => void;
};

type TimezoneInfo = {
timezone: string;
offset: string;
};

export default ({ value, onChange, setDateTime }: Props) => {
const [date, setDate] = useState("");
const [time, setTime] = useState("");
const [selectedTimezone, setSelectedTimezone] = useState<TimezoneInfo>({
offset: "+0:00",
timezone: "Africa/Abidjan",
});

const handleTimeChange = useCallback((newValue: string | undefined) => {
if (newValue === undefined) return;
setTime(newValue);
}, []);

const handleDateChange = useCallback((newValue: string | undefined) => {
if (newValue === undefined) return;
setDate(newValue);
}, []);

const offsetFromUTC: TimezoneInfo[] = useMemo(() => {
return getUniqueTimezones(moment.tz.names());
}, []);

const handleApplyChange = useCallback(() => {
const selectedTimezoneInfo = offsetFromUTC.find(
info => info.timezone === selectedTimezone.timezone,
);
if (selectedTimezoneInfo) {
const formattedDateTime = `${date}T${time}:00${selectedTimezoneInfo.offset}`;
setDateTime?.(formattedDateTime);
onChange?.(formattedDateTime);
}
}, [offsetFromUTC, selectedTimezone, date, time, setDateTime, onChange]);

const handleTimezoneSelect = useCallback(
(newValue: string) => {
const updatedTimezone = offsetFromUTC.find(info => info.timezone === newValue);
setSelectedTimezone(updatedTimezone || selectedTimezone);
},
[offsetFromUTC, selectedTimezone],
);

useEffect(() => {
if (value) {
const [parsedDate, timeWithOffset] = value.split("T");
const [parsedTime, timezoneOffset] = timeWithOffset.split(/[-+]/);

setDate(parsedDate);
setTime(parsedTime);

const updatedTimezone = offsetFromUTC.find(
info =>
info.offset ===
(timeWithOffset.includes("-") ? `-${timezoneOffset}` : `+${timezoneOffset}`),
);
updatedTimezone && setSelectedTimezone(updatedTimezone);
} else {
setDate("");
setTime("");
setSelectedTimezone({ offset: "+0:00", timezone: "Africa/Abidjan" });
}
}, [value, offsetFromUTC]);

return {
date,
time,
selectedTimezone,
offsetFromUTC,
onTimeChange: handleTimeChange,
onTimezoneSelect: handleTimezoneSelect,
onDateChange: handleDateChange,
onDateTimeApply: handleApplyChange,
};
};
120 changes: 120 additions & 0 deletions web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useMemo } from "react";

import Button from "@reearth/beta/components/Button";
import PanelCommon from "@reearth/beta/components/fields/common/PanelCommon";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import TextInput from "../../common/TextInput";
import SelectField from "../../SelectField";

import useHooks from "./hooks";

type Props = {
onChange?: (value?: string | undefined) => void;
onClose: () => void;
value?: string;
setDateTime?: (value?: string | undefined) => void;
};

const EditPanel: React.FC<Props> = ({ onChange, onClose, value, setDateTime }) => {
const t = useT();

const {
date,
time,
selectedTimezone,
offsetFromUTC,
onDateChange,
onTimeChange,
onTimezoneSelect,
onDateTimeApply,
} = useHooks({ value, onChange, setDateTime });

const isButtonDisabled = useMemo(() => {
return date.trim() === "" || time.trim() === "";
}, [date, time]);

return (
<PanelCommon title={t("Set Time")} onClose={onClose}>
<FieldGroup>
<TextWrapper>
<Label>{t("Date")}</Label>
<Input type="date" value={date} onChange={onDateChange} />
</TextWrapper>
<TextWrapper>
<Label>{t("Time")}</Label>

<Input type="time" value={time} onChange={onTimeChange} />
</TextWrapper>
<SelectWrapper>
<Label>{t("Time Zone")}</Label>
<CustomSelect
value={selectedTimezone.timezone}
options={offsetFromUTC.map(timezone => ({
key: timezone.timezone,
label: timezone?.offset,
}))}
onChange={onTimezoneSelect}
/>
</SelectWrapper>
</FieldGroup>
<Divider />
<ButtonWrapper>
<StyledButton text={t("Cancel")} size="small" onClick={onClose} />
<StyledButton
text={t("Apply")}
size="small"
buttonType="primary"
onClick={() => {
onDateTimeApply(), onClose();
}}
disabled={isButtonDisabled}
/>
</ButtonWrapper>
</PanelCommon>
);
};

const TextWrapper = styled.div`
margin-left: 8px;
width: 88%;
`;

const Input = styled(TextInput)`
width: 100%;
`;

const FieldGroup = styled.div`
padding-bottom: 8px;
`;

const Label = styled.div`
font-size: 12px;
padding: 10px 0;
`;

const Divider = styled.div`
border-top: 1px solid ${({ theme }) => theme.outline.weak};
`;

const ButtonWrapper = styled.div`
display: flex;
gap: 8px;
padding: 8px;
`;

const StyledButton = styled(Button)`
flex: 1;
`;

const SelectWrapper = styled.div`
margin-left: 8px;
width: 95%;
`;
const CustomSelect = styled(SelectField)`
height: 120px;
overflow-y: auto;
width: 100%;
`;
export default EditPanel;
117 changes: 94 additions & 23 deletions web/src/beta/components/fields/DateTimeField/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";

import Button from "@reearth/beta/components/Button";
import Icon from "@reearth/beta/components/Icon";
import * as Popover from "@reearth/beta/components/Popover";
import Text from "@reearth/beta/components/Text";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import Property from "..";
import TextInput from "../common/TextInput";

import EditPanel from "./EditPanel";

export type Props = {
name?: string;
Expand All @@ -13,34 +19,60 @@ export type Props = {
};

const DateTimeField: React.FC<Props> = ({ name, description, value, onChange }) => {
const [time, setTime] = useState<string>(value?.split(" ")[1] ?? "HH:MM:SS");
const [date, setDate] = useState<string>(value?.split(" ")[0] ?? "YYYY-MM-DD");
const [open, setOpen] = useState(false);
const t = useT();

const handleTimeChange = useCallback(
(newValue: string | undefined) => {
if (newValue === undefined) return;
const handlePopOver = useCallback(() => setOpen(!open), [open]);
const handleRemoveSetting = useCallback(() => {
if (!value) return;
setDateTime("");
onChange?.();
}, [value, onChange]);

setTime(newValue);
onChange?.(date + " " + newValue);
},
[date, onChange],
);
const [dateTime, setDateTime] = useState(value);

const handleDateChange = useCallback(
(newValue: string | undefined) => {
if (newValue === undefined) return;

setDate(newValue);
onChange?.(newValue + " " + time);
},
[time, onChange],
);
useEffect(() => {
setDateTime(value);
}, [value]);

return (
<Property name={name} description={description}>
<Wrapper>
<TextInput type="date" value={date} onChange={handleDateChange} />
<TextInput type="time" value={time} onChange={handleTimeChange} />
<Popover.Provider open={!!open} placement="bottom-start">
<Popover.Trigger asChild>
<InputWrapper disabled={true}>
<Input dataTimeSet={!!dateTime}>
<Text size="footnote" customColor>
{dateTime ? dateTime : "YYYY-MM-DDThh:mm:ss±hh:mm"}
</Text>
<DeleteIcon
icon="bin"
size={10}
disabled={!dateTime}
onClick={handleRemoveSetting}
/>
</Input>
<TriggerButton
buttonType="secondary"
text={t("set")}
icon="clock"
size="small"
iconPosition="left"
onClick={() => handlePopOver()}
/>
</InputWrapper>
</Popover.Trigger>
<PopoverContent autoFocus={false}>
{open && (
<EditPanel
setDateTime={setDateTime}
value={dateTime}
onChange={onChange}
onClose={handlePopOver}
/>
)}
</PopoverContent>
</Popover.Provider>
</Wrapper>
</Property>
);
Expand All @@ -53,3 +85,42 @@ const Wrapper = styled.div`
align-items: stretch;
gap: 4px;
`;

const InputWrapper = styled.div<{ disabled?: boolean }>`
display: flex;
width: 100%;
gap: 10px;
height: 28px;
`;

const Input = styled.div<{ dataTimeSet?: boolean }>`
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
flex: 1;
padding: 0 8px;
border-radius: 4px;
border: 1px solid ${({ theme }) => theme.outline.weak};
color: ${({ theme }) => theme.content.strong};
background: ${({ theme }) => theme.bg[1]};
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25) inset;

color: ${({ theme, dataTimeSet }) => (dataTimeSet ? theme.content.strong : theme.content.weak)};
`;

const TriggerButton = styled(Button)`
margin: 0;
`;

const PopoverContent = styled(Popover.Content)`
z-index: 701;
`;
const DeleteIcon = styled(Icon)<{ disabled?: boolean }>`
${({ disabled, theme }) =>
disabled
? `color: ${theme.content.weaker};`
: `:hover {
cursor: pointer;
}`}
`;
Loading
Loading