[front]: gitignore fix

This commit is contained in:
it-is-not-alright 2024-10-22 23:11:03 +04:00
parent 52eef8cfbf
commit 17ba979e41
16 changed files with 443 additions and 1 deletions

1
.gitignore vendored
View File

@ -17,7 +17,6 @@ eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/

View File

@ -0,0 +1,39 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import styles from './style.module.scss';
import { RippleWaveProps } from './types';
export function RippleWave({ style, onDone }: RippleWaveProps) {
const [isMouseUp, setIsMouseUp] = useState(false);
const [isAnimationEnd, setIsAnimationEnd] = useState(false);
useEffect(() => {
const mouseUpListener = () => setIsMouseUp(true);
document.addEventListener('mouseup', mouseUpListener, { once: true });
document.addEventListener('touchend', mouseUpListener, { once: true });
}, []);
const visible = !isMouseUp || !isAnimationEnd;
const className = clsx(
styles.wave,
visible ? styles.visible : styles.invisible,
);
const handleAnimationEnd = (event: React.AnimationEvent) => {
if (event.animationName === styles.fadein) {
setIsAnimationEnd(true);
} else {
onDone();
}
};
return (
<div
className={className}
onAnimationEnd={handleAnimationEnd}
style={style}
/>
);
}

View File

@ -0,0 +1 @@
export { RippleWave } from './component';

View File

@ -0,0 +1,33 @@
.wave {
position: absolute;
border-radius: 100%;
background-color: var(--clr-ripple);
}
.visible {
animation: fadein 0.3s linear;
}
.invisible {
animation: fadeout 0.3s linear forwards;
}
@keyframes fadein {
from {
opacity: 0;
scale: 0;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes fadeout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@ -0,0 +1,6 @@
import { CSSProperties } from 'react';
export type RippleWaveProps = {
style: CSSProperties;
onDone: () => void;
};

View File

@ -0,0 +1,81 @@
import { IconButton } from '@components/ui/icon-button';
import { RawButton } from '@components/ui/raw';
import { Span } from '@components/ui/span';
import ArrowDownIcon from '@public/images/svg/arrow-down.svg';
import ArrowUpIcon from '@public/images/svg/arrow-up.svg';
import clsx from 'clsx';
import React, { useMemo } from 'react';
import { DAYS_OF_THE_WEEK, MONTHS } from './constants';
import styles from './styles.module.scss';
import { CalendarDaysProps } from './types';
import { getCalendarDays } from './utils';
export function CalendarDays({
value,
onChange,
min,
max,
date,
onMonthChange,
}: CalendarDaysProps) {
const today = useMemo(() => new Date(), []);
const days = useMemo(() => {
return getCalendarDays({
year: date.getFullYear(),
monthIndex: date.getMonth(),
today,
selectedDateStr: value,
min,
max,
});
}, [date, min, max]);
const handleChange = (newValue: string) => {
onChange?.(newValue);
};
return (
<div>
<header className={styles.header}>
<Span color="t300" className={styles.title}>
{MONTHS[date.getMonth()]} {date.getFullYear()}
</Span>
<IconButton
className={styles.turnButton}
onClick={() => onMonthChange(-1)}
>
<ArrowUpIcon />
</IconButton>
<IconButton
className={styles.turnButton}
onClick={() => onMonthChange(1)}
>
<ArrowDownIcon />
</IconButton>
</header>
<div className={styles.daysGrid}>
{DAYS_OF_THE_WEEK.map((day) => (
<Span className={styles.dayOfTheWeek} scale="none" key={day}>
{day}
</Span>
))}
{days.map((day, index) => (
<RawButton
key={index}
disabled={day.isDisabled}
className={clsx(styles.day, {
[styles.selectedDay]: day.isSelected,
[styles.currentMonthDay]: day.isCurrentMonth,
})}
onClick={() => handleChange(day.string)}
>
{day.number}
{day.isToday && <div className={styles.todayIndicator} />}
</RawButton>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
export const DAYS_OF_THE_WEEK = [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
];
export const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];

View File

@ -0,0 +1,2 @@
export { CalendarDays } from './component';
export { type CalendarDaysProps } from './types';

View File

@ -0,0 +1,85 @@
.header {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.title {
flex: 1;
padding-left: 5px;
font-size: 18px;
font-weight: 500;
}
.turnButton {
padding: 10px;
}
.daysGrid {
display: grid;
gap: 5px;
grid-template-columns: repeat(7, auto);
}
.dayOfTheWeek {
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
font-size: 14px;
}
.day {
position: relative;
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
border-radius: 10px;
color: var(--clr-text-100);
transition: all var(--td-100) ease-in-out;
&:not(:disabled) {
cursor: pointer;
&:hover {
background-color: var(--clr-layer-300-hover);
}
}
&:disabled {
color: var(--clr-text-100);
}
}
.currentMonthDay {
color: var(--clr-text-300);
}
.selectedDay {
background-color: var(--clr-primary);
box-shadow: 0px 2px 2px var(--clr-shadow-200);
color: var(--clr-on-primary);
&:hover {
background-color: var(--clr-primary-hover);
}
.todayIndicator {
background-color: var(--clr-on-primary);
}
}
.todayIndicator {
position: absolute;
bottom: 12%;
left: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--clr-text-300);
transform: translateX(-50%);
}

View File

@ -0,0 +1,26 @@
export type CalendarDay = {
number: number;
isDisabled: boolean;
isSelected: boolean;
isToday: boolean;
string: string;
isCurrentMonth: boolean;
};
export type CalendarDaysProps = {
value?: string;
onChange?: (value: string) => void;
min: Date | null;
max: Date | null;
date: Date;
onMonthChange: (delta: number) => void;
};
export type GetCalendarDaysParams = {
year: number;
monthIndex: number;
today: Date;
selectedDateStr?: string;
min: Date | null;
max: Date | null;
};

View File

@ -0,0 +1,52 @@
import { dateToInputString } from '@utils/date';
import { CalendarDay, GetCalendarDaysParams } from './types';
const addDays = (date: Date, days: number) => {
date.setDate(date.getDate() + days);
};
const daysAreEqual = (date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
};
export const getCalendarDays = ({
year,
monthIndex,
today,
selectedDateStr,
min,
max,
}: GetCalendarDaysParams) => {
const selectedDate = new Date(selectedDateStr);
const firstDayOfMonth = new Date(year, monthIndex, 1);
const daysFromPrevMonth = (firstDayOfMonth.getDay() || 7) - 1;
const date = new Date(year, monthIndex, 1);
addDays(date, -daysFromPrevMonth);
const days: CalendarDay[] = [];
for (let i = 0; i < 42; i += 1) {
const number = date.getDate();
const isDisabled = (min && date < min) || (max && date > max);
const isSelected = daysAreEqual(date, selectedDate);
const isToday = daysAreEqual(date, today);
const string = dateToInputString(date);
const isCurrentMonth = date.getMonth() === monthIndex;
days.push({
number,
isDisabled,
isSelected,
isToday,
string,
isCurrentMonth,
});
addDays(date, 1);
}
return days;
};

View File

@ -0,0 +1 @@
export * from './calendar-days';

View File

@ -0,0 +1,68 @@
import { Checkbox } from '@components/ui';
import { Input } from '@components/ui/input';
import { WindmillConfig } from '@components/ux/windmill-form';
import React from 'react';
import styles from './style.module.scss';
import { WindmillTableRowProps } from './types';
export function WindmillTableRow({
value,
onChange,
onSelect,
selected,
}: WindmillTableRowProps) {
const extractNumber = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (!value) {
return '';
}
const regex = /^[-+]?\d*\.?\d*$/;
return event.target.value.match(regex)?.[0] ?? null;
};
const handleChange = (
event: React.ChangeEvent<HTMLInputElement>,
min: number,
max: number,
key: keyof WindmillConfig,
) => {
const num = extractNumber(event);
if (num === null) {
return;
}
const float = parseFloat(num);
if (Number.isNaN(float)) {
onChange({ ...value, [key]: num });
return;
}
if (float >= min && float <= max) {
onChange({ ...value, [key]: num });
}
};
return (
<div className={styles.row}>
<Checkbox
label={{ className: styles.checkboxLabel }}
onChange={onSelect}
checked={selected}
/>
<Input
wrapper={{ className: styles.textInput }}
value={value.x}
onChange={(event) => handleChange(event, -90, 90, 'x')}
/>
<Input
wrapper={{ className: styles.textInput }}
value={value.y}
onChange={(event) => handleChange(event, -180, 180, 'y')}
/>
<Input
wrapper={{ className: styles.textInput }}
value={value.angle}
onChange={(event) => handleChange(event, 0, 360, 'angle')}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export { WindmillTableRow } from './component';

View File

@ -0,0 +1,16 @@
.row {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr;
}
.checkboxLabel {
width: 46px;
justify-content: center;
border: 1px solid var(--clr-border-200);
}
.textInput {
border-radius: 0;
background-color: var(--clr-layer-200);
box-shadow: none;
}

View File

@ -0,0 +1,8 @@
import { WindmillConfig } from '@components/ux/windmill-form';
export type WindmillTableRowProps = {
value: WindmillConfig;
onChange: (value: WindmillConfig) => void;
onSelect: () => void;
selected: boolean;
};