mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 14:22:25 +03:00
Add settings to enable 24-hour time format and customizable date format (#2347)
* Add setting to enable 24-hour time format * added hour24Clock to TimeProps * Add incomplete dateFormatString setting * Move 24-hour toggle to Appearance * Add "Date & Time" subheading, cleanup after merge * Add setting for date formatting * Fix minor formatting and naming issues * Document functions * adress most comments * add hint for date formatting * add support for 24hr time to TimePicker * prevent overflow on small displays
This commit is contained in:
parent
67b05eeb09
commit
9183fd66b2
17 changed files with 691 additions and 82 deletions
|
@ -1,15 +1,19 @@
|
|||
import React, {
|
||||
ChangeEventHandler,
|
||||
FormEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
as,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
config,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
|
@ -28,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
|
@ -44,6 +48,7 @@ import {
|
|||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
|
@ -341,6 +346,359 @@ function Appearance() {
|
|||
);
|
||||
}
|
||||
|
||||
type DateHintProps = {
|
||||
hasChanges: boolean;
|
||||
handleReset: () => void;
|
||||
};
|
||||
function DateHint({ hasChanges, handleReset }: DateHintProps) {
|
||||
const [anchor, setAnchor] = useState<RectCords>();
|
||||
const categoryPadding = { padding: config.space.S200, paddingTop: 0 };
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||
setAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
return (
|
||||
<PopOut
|
||||
anchor={anchor}
|
||||
position="Top"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ maxHeight: '85vh', overflowY: 'auto' }}>
|
||||
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
|
||||
<Text size="L400">Formatting</Text>
|
||||
</Header>
|
||||
|
||||
<Box direction="Column">
|
||||
<Box style={categoryPadding} direction="Column">
|
||||
<Header size="300">
|
||||
<Text size="L400">Year</Text>
|
||||
</Header>
|
||||
<Box direction="Column" tabIndex={0} gap="100">
|
||||
<Text size="T300">
|
||||
YY
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}
|
||||
Two-digit year
|
||||
</Text>{' '}
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
YYYY
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Four-digit year
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box style={categoryPadding} direction="Column">
|
||||
<Header size="300">
|
||||
<Text size="L400">Month</Text>
|
||||
</Header>
|
||||
<Box direction="Column" tabIndex={0} gap="100">
|
||||
<Text size="T300">
|
||||
M
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}The month
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
MM
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Two-digit month
|
||||
</Text>{' '}
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
MMM
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Short month name
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
MMMM
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Full month name
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box style={categoryPadding} direction="Column">
|
||||
<Header size="300">
|
||||
<Text size="L400">Day of the Month</Text>
|
||||
</Header>
|
||||
<Box direction="Column" tabIndex={0} gap="100">
|
||||
<Text size="T300">
|
||||
D
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Day of the month
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
DD
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Two-digit day of the month
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box style={categoryPadding} direction="Column">
|
||||
<Header size="300">
|
||||
<Text size="L400">Day of the Week</Text>
|
||||
</Header>
|
||||
<Box direction="Column" tabIndex={0} gap="100">
|
||||
<Text size="T300">
|
||||
d
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Day of the week (Sunday = 0)
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
dd
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Two-letter day name
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
ddd
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Short day name
|
||||
</Text>
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
dddd
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
{': '}Full day name
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{hasChanges ? (
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
onClick={handleReset}
|
||||
type="reset"
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon src={Icons.Cross} size="100" />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
onClick={handleOpenMenu}
|
||||
type="button"
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-pressed={!!anchor}
|
||||
>
|
||||
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
|
||||
</IconButton>
|
||||
)}
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
type CustomDateFormatProps = {
|
||||
value: string;
|
||||
onChange: (format: string) => void;
|
||||
};
|
||||
function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
|
||||
const [dateFormatCustom, setDateFormatCustom] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setDateFormatCustom(value);
|
||||
}, [value]);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
const format = evt.currentTarget.value;
|
||||
setDateFormatCustom(format);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setDateFormatCustom(value);
|
||||
};
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const target = evt.target as HTMLFormElement | undefined;
|
||||
const customDateFormatInput = target?.customDateFormatInput as HTMLInputElement | undefined;
|
||||
const format = customDateFormatInput?.value;
|
||||
if (!format) return;
|
||||
|
||||
onChange(format);
|
||||
};
|
||||
|
||||
const hasChanges = dateFormatCustom !== value;
|
||||
return (
|
||||
<SettingTile>
|
||||
<Box as="form" onSubmit={handleSubmit} gap="200">
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Input
|
||||
required
|
||||
name="customDateFormatInput"
|
||||
value={dateFormatCustom}
|
||||
onChange={handleChange}
|
||||
maxLength={16}
|
||||
autoComplete="off"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
style={{ paddingRight: config.space.S200 }}
|
||||
after={<DateHint hasChanges={hasChanges} handleReset={handleReset} />}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
size="400"
|
||||
variant={hasChanges ? 'Success' : 'Secondary'}
|
||||
fill={hasChanges ? 'Solid' : 'Soft'}
|
||||
outlined
|
||||
radii="300"
|
||||
disabled={!hasChanges}
|
||||
type="submit"
|
||||
>
|
||||
<Text size="B400">Save</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</SettingTile>
|
||||
);
|
||||
}
|
||||
|
||||
type PresetDateFormatProps = {
|
||||
value: string;
|
||||
onChange: (format: string) => void;
|
||||
};
|
||||
function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const dateFormatItems = useDateFormatItems();
|
||||
|
||||
const getDisplayDate = (format: string): string =>
|
||||
format !== '' ? dayjs().format(format) : 'Custom';
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (format: DateFormat) => {
|
||||
onChange(format);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">
|
||||
{getDisplayDate(dateFormatItems.find((i) => i.format === value)?.format ?? value)}
|
||||
</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{dateFormatItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.format}
|
||||
size="300"
|
||||
variant={value === item.format ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
onClick={() => handleSelect(item.format)}
|
||||
>
|
||||
<Text size="T300">{getDisplayDate(item.format)}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectDateFormat() {
|
||||
const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
|
||||
const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
|
||||
const customDateFormat = selectedDateFormat === '';
|
||||
|
||||
const handlePresetChange = (format: string) => {
|
||||
setSelectedDateFormat(format);
|
||||
if (format !== '') {
|
||||
setDateFormatString(format);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingTile
|
||||
title="Date Format"
|
||||
description={customDateFormat ? dayjs().format(dateFormatString) : ''}
|
||||
after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
|
||||
/>
|
||||
{customDateFormat && (
|
||||
<CustomDateFormat value={dateFormatString} onChange={setDateFormatString} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DateAndTime() {
|
||||
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Date & Time</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="24-Hour Time Format"
|
||||
after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SelectDateFormat />
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Editor() {
|
||||
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
|
||||
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||
|
@ -637,6 +995,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Appearance />
|
||||
<DateAndTime />
|
||||
<Editor />
|
||||
<Messages />
|
||||
</Box>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue