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

680 update opening hours #832

Merged
merged 13 commits into from
Jan 9, 2024
42 changes: 42 additions & 0 deletions frontend/src/Components/InputTime/InputTime.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import 'src/constants';

@import 'src/mixins';

.inputTime {
display: flex;
align-items: left;
}

.number {
width: 2em;
height: 1.5em;
box-sizing: border-box;
padding: auto 5px;
text-align: center;
border: 2px solid $grey-4;
@include theme-dark {
border-color: $grey-2;
}
}

/* Chrome, Safari, Edge, Opera */
.number::-webkit-outer-spin-button,
.number::-webkit-inner-spin-button {
appearance: none;
}

/* Firefox */
.number[type='number'] {
-moz-appearance: textfield;
}

.inputTime,
.number {
border-radius: 8px;
font-size: 1em;
}

.number:focus {
outline: 3px hidden var(--active);
box-shadow: 0 0 2px 2px #999999;
robines marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 14 additions & 0 deletions frontend/src/Components/InputTime/InputTime.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { InputTime } from './InputTime';

export default {
title: 'Components/InputTime',
component: InputTime,
} as ComponentMeta<typeof InputTime>;

const Template: ComponentStory<typeof InputTime> = function (args) {
return <InputTime {...args}>Option</InputTime>;
};

export const Basic = Template.bind({});
Basic.args = {};
79 changes: 79 additions & 0 deletions frontend/src/Components/InputTime/InputTime.tsx
emilte marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ChangeEvent, useEffect, useState } from 'react';
import styles from './InputTime.module.scss';

type InputTimeProps = {
className?: string;
disabled?: boolean;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
value?: string;
};

export function InputTime({ onChange, onBlur, value }: InputTimeProps) {
const [hour, setHour] = useState('');
const [minute, setMinute] = useState('');

useEffect(() => {
if (value) {
const [parsedHour, parsedMinute] = value.split(':');
setHour(parsedHour);
setMinute(parsedMinute);
}
}, [value]);

useEffect(() => {
const formattedTime = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
onChange?.(formattedTime);
}, [hour, minute, onChange]);

function handleChange(e: ChangeEvent<HTMLInputElement>) {
const inputName = e.target.getAttribute('name');
let numericValue = e.target.value.replace(/[^0-9]/g, '');
const parsedValue = parseInt(numericValue, 10);
if (inputName === 'hour') {
numericValue = parsedValue > 23 ? '23' : e.target.value;
// Regex for 00-23, allowing for values without 0 padding
if (/^(2[0-3]|[0-1]?[0-9])$/.test(numericValue) || numericValue.length === 0) {
setHour(numericValue);
onChange?.(numericValue.padStart(2, '0') + ':' + minute.padStart(2, '0'));
}
} else if (inputName === 'minute') {
numericValue = parsedValue > 59 ? '59' : e.target.value;
// Regex for 00-59, allowing for values without 0 padding
if (/^([0-5]?[0-9])$/.test(numericValue) || numericValue.length === 0) {
setMinute(numericValue);
onChange?.(hour.padStart(2, '0') + ':' + numericValue.padStart(2, '0'));
}
}
}

function handleBlur() {
const formattedTime = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
onBlur?.(formattedTime);
}

return (
<div className={styles.inputTime_wrap}>
<div className={styles.inputTime}>
<input
type="text"
className={styles.number}
name="hour"
value={hour}
onChange={handleChange}
onBlur={handleBlur}
/>
<p>:</p>
<input
type="text"
className={styles.number}
name="minute"
value={minute}
onChange={handleChange}
onBlur={handleBlur}
/>
</div>
<div className={styles.error}></div>
</div>
);
}
1 change: 1 addition & 0 deletions frontend/src/Components/InputTime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InputTime } from './InputTime';
1 change: 1 addition & 0 deletions frontend/src/Components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { ImageList } from './ImageList';
export { ImageQuery } from './ImageQuery';
export { InputField } from './InputField';
export { InputFile } from './InputFile';
export { InputTime } from './InputTime';
export { Link } from './Link';
export { List } from './List';
export { Modal } from './Modal';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ $bg-dark: #272727;
}

.venue_box {
border-radius: .4em;
border-radius: 0.4em;
background-color: $white;
border: 1px solid $grey-4;
overflow: hidden;
min-width: 16em;
max-width: 16em;
max-width: 20em;

@include theme-dark {
border-color: $black;
Expand All @@ -33,7 +33,7 @@ $bg-dark: #272727;
}

.venue_header {
padding: .5em .75em;
padding: 0.5em 0.75em;
border-bottom: 1px solid $header-border;
font-size: 1.25em;
font-weight: 700;
Expand All @@ -54,7 +54,7 @@ $bg-dark: #272727;
flex-direction: column;
justify-content: space-between;
width: 100%;
gap: .5em;
gap: 0.5em;
}

.day_row {
Expand All @@ -72,9 +72,5 @@ $bg-dark: #272727;

.day_edit {
@include flex-row;
gap: .5em;
}

.invalid {
border: 2px solid red;
gap: 0.5em;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { InputTime } from '~/Components';
import { getVenues, putVenue } from '~/api';
import { VenueDto } from '~/dto';
import { KEY } from '~/i18n/constants';
Expand All @@ -14,7 +14,6 @@ export function OpeningHoursAdminPage() {
const { t } = useTranslation();
const [venues, setVenues] = useState<VenueDto[]>([]);
const [saveTimer, setSaveTimer] = useState<Record<string, NodeJS.Timeout>>({});
const [invalid, setInvalid] = useState<Record<string, boolean>>({});
const [isLoading, setIsLoading] = useState<boolean>(true);

// We need a reference to read changed state inside timeout
Expand All @@ -37,30 +36,14 @@ export function OpeningHoursAdminPage() {
}, []);

// Save venue change.
function saveVenue(venue: VenueDto, field: keyof VenueDto) {
function saveVenue(venue: VenueDto, field: keyof VenueDto, value: string) {
// Get most recent edits if any
const recent = venueRef.current.filter((v) => v.id === venue.id)[0];
const updatedVenues = venues.map((v) => (v.id === venue.id ? { ...v, [field]: value } : v));
venueRef.current = updatedVenues;
// Send field change to backend
putVenue(venue.id, {
[field]: recent[field],
})
// Success.
.then(() => {
setInvalid({
...invalid,
[`${venue.id}_${field}`]: false,
});
toast.success(t(KEY.common_update_successful));
})
// Failed
.catch((error) => {
setInvalid({
...invalid,
[`${venue.id}_${field}`]: true,
});
toast.error(t(KEY.common_something_went_wrong));
console.error(error);
});
putVenue(venue.slug, {
[field]: value,
});
}

// Update view model on field.
Expand All @@ -77,7 +60,6 @@ export function OpeningHoursAdminPage() {
}
return v;
});

// Update state and reference.
setVenues(newVenues);
venueRef.current = newVenues;
Expand All @@ -90,7 +72,7 @@ export function OpeningHoursAdminPage() {

// Start a new save timer.
const timer = setTimeout(() => {
saveVenue(venue, field);
saveVenue(venue, field, value);
}, 1000);

// Store timeout to allow cancel.
Expand All @@ -114,29 +96,22 @@ export function OpeningHoursAdminPage() {
{ALL_DAYS.map((day) => {
const openField: keyof VenueDto = `opening_${day}`;
const closeField: keyof VenueDto = `closing_${day}`;
// Error checking
const invalidOpen = invalid[`${venue.id}_${openField}`] === true;
const invalidClose = invalid[`${venue.id}_${closeField}`] === true;
// Edit tools
return (
<div key={day} className={styles.day_row}>
<div className={styles.day_label}>{t(getDayKey(day))}</div>
<div className={styles.day_edit}>
<input
type="time"
<InputTime
value={venue[openField]}
onChange={handleOnChange(venue, openField)}
onBlur={() => saveVenue(venue, openField)}
className={classNames(invalidOpen && styles.invalid)}
/>
-
<input
type="time"
onChange={() => handleOnChange(venue, openField)}
onBlur={(formattedTime) => saveVenue(venue, openField, formattedTime)}
></InputTime>
<p>-</p>
<InputTime
value={venue[closeField]}
onChange={handleOnChange(venue, closeField)}
onBlur={() => saveVenue(venue, closeField)}
className={classNames(invalidClose && styles.invalid)}
/>
onChange={() => handleOnChange(venue, closeField)}
onBlur={(formattedTime) => saveVenue(venue, closeField, formattedTime)}
></InputTime>
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ export async function getVenue(id: string | number): Promise<VenueDto> {
return response.data;
}

export async function putVenue(id: string | number, venue: Partial<VenueDto>): Promise<VenueDto> {
const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__venues_detail, urlParams: { pk: id } });
export async function putVenue(slug: string | number, venue: Partial<VenueDto>): Promise<VenueDto> {
const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__venues_detail, urlParams: { slug: slug } });
const response = await axios.put<VenueDto>(url, venue, { withCredentials: true });
return response.data;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type ObjectPermissionDto = {

export type VenueDto = {
id: number;
slug: string;
name: string;
description?: string;
floor?: number;
Expand Down
Loading