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

Implement pingers page #2

Merged
merged 14 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"test": "react-scripts test"
},
"dependencies": {
"@apollo/client": "^3.3.8",
"@apollo/client": "^3.10.8",
"@brokalys/location-json-schemas": "^1.2.0",
"@bugsnag/js": "^7.6.0",
"@bugsnag/plugin-react": "^7.6.0",
Expand Down
45 changes: 3 additions & 42 deletions src/components/Form/Fields/RegionField/RegionField.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
import { useState } from "react";
import {
DropdownItemProps,
DropdownProps,
Form,
Select,
} from "semantic-ui-react";
import { DropdownProps, Form, Select } from "semantic-ui-react";
import RegionSelector from "components/RegionSelector";
import styles from "./RegionField.module.css";
import useRegionOptions from "./use-region-options";

function getCenterCoords(arr: Array<[number, number]>) {
const coords = arr.reduce(
(x, y) => [x[0] + y[0] / arr.length, x[1] + y[1] / arr.length],
[0, 0],
);

return { lat: coords[1], lng: coords[0] };
}

function getCoordinatesByValue(
options: DropdownItemProps[] | undefined,
value: any,
) {
return options!.find((opt) => opt.value === value)?.data?.geometry
?.coordinates[0];
}

const DEFAULT_ZOOM = 11;
const DEFAULT_CENTER = {
lat: 56.94,
lng: 24.105,
};

interface RegionFieldProps {
value: string;
onChange: (value: any) => void;
Expand All @@ -40,26 +12,20 @@ interface RegionFieldProps {
export default function RegionField(props: RegionFieldProps) {
const regionOptions = useRegionOptions();
const [initialValue] = useState(() => props.value);
const [center, setCenter] = useState(DEFAULT_CENTER);
const [zoom, setZoom] = useState(DEFAULT_ZOOM);

function onSelectChange(
e: React.SyntheticEvent<HTMLElement>,
{ value, options }: DropdownProps,
{ value }: DropdownProps,
) {
if (!value) {
reset();
return;
}

setCenter(getCenterCoords(getCoordinatesByValue(options, value)));
setZoom(13);
props.onChange(value);
}

function reset() {
setZoom(DEFAULT_ZOOM);
setCenter(DEFAULT_CENTER);
props.onChange(initialValue);
}

Expand All @@ -81,12 +47,7 @@ export default function RegionField(props: RegionFieldProps) {
/>
</div>

<RegionSelector
center={center}
zoom={zoom}
value={props.value}
onChange={props.onChange}
/>
<RegionSelector value={props.value} onChange={props.onChange} />
</Form.Field>
</>
);
Expand Down
135 changes: 70 additions & 65 deletions src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,59 @@ import React, { useState } from "react";
import { Controller, FieldError, useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import {
DropdownItemProps,
DropdownProps,
Form,
Grid,
Icon,
Label,
LabelProps,
SemanticShorthandItem,
StrictDropdownItemProps,
} from "semantic-ui-react";
import { yupResolver } from "@hookform/resolvers/yup";
import "shared/l10n";
import SupportButton from "components/SupportButton";
import RegionField from "./Fields/RegionField";
import PriceTypeLabel from "./PriceTypeLabel";
import schema, { FormSchema, PRICE_TYPE } from "./schema";
import schema, { PingerSchema, PRICE_TYPE } from "./schema";
import { TRANSLATION_MAP } from "../../shared/l10n";

const categoryOptions: DropdownItemProps[] = [
{ value: "APARTMENT", text: "Dzīvoklis" },
{ value: "HOUSE", text: "Māja" },
{ value: "LAND", text: "Zeme" },
];

const typeOptions: DropdownItemProps[] = [
{ value: "SELL", text: "Pārdod" },
{ value: "RENT", text: "Īrē" },
{ value: "AUCTION", text: "Izsole" },
];
const frequencyDescription: Record<
keyof typeof TRANSLATION_MAP["frequency"],
string
> = {
IMMEDIATE: "(viens e-pasts par katru jauno sludinājumu)",
DAILY: "(viens e-pasts ar visiem dienas sludinājumiem)",
WEEKLY: "(viens e-pasts ar visiem nedēļas sludinājumiem)",
MONTHLY: "(viens e-pasts ar visiem mēneša sludinājumiem)",
};

const priceTypeOptions: DropdownItemProps[] = [
{ value: "TOTAL", text: "Kopējā cena" },
{ value: "SQM", text: "Par kvadrātmetru" },
];
const toDropDownItemProps = (
obj: Record<string, string>,
): StrictDropdownItemProps[] => {
return Object.entries(obj).map(([key, value]) => ({
value: key,
text: value,
}));
};

const frequencyOptions: DropdownItemProps[] = [
{
value: "IMMEDIATE",
text: "Nekavējoties",
description: "(viens e-pasts par katru jauno sludinājumu)",
},
{
value: "DAILY",
text: "Reizi dienā",
description: "(viens e-pasts ar visiem dienas sludinājumiem)",
const categoryOptions = toDropDownItemProps(TRANSLATION_MAP.category);
const typeOptions = toDropDownItemProps(TRANSLATION_MAP.type);
const priceTypeOptions = toDropDownItemProps(TRANSLATION_MAP.price);
const frequencyOptions = toDropDownItemProps(TRANSLATION_MAP.frequency).map(
(v) => {
return {
...v,
description:
frequencyDescription[
// TODO: I suspect with TS 5.5 casting might be fixed
v.value as keyof typeof TRANSLATION_MAP["frequency"]
],
};
},
{
value: "WEEKLY",
text: "Reizi nedēļā",
description: "(viens e-pasts ar visiem nedēļas sludinājumiem)",
},
{
value: "MONTHLY",
text: "Reizi mēnesī",
description: "(viens e-pasts ar visiem mēneša sludinājumiem)",
},
];
);

const DEFAULT_PRICE_TYPE = "TOTAL";
const DEFAULT_PRICE_TYPE: keyof typeof TRANSLATION_MAP["price"] = "TOTAL";

function getError(
field?: FieldError,
Expand All @@ -71,16 +67,19 @@ function getError(
}

interface PingerFormProps {
onSubmit: (data: FormSchema) => void;
pinger?: PingerSchema;
onSubmit: (data: PingerSchema) => void;
loading?: boolean;
error?: React.ReactNode;
warning?: React.ReactNode;
success?: React.ReactNode;
}

export default function PingerForm(props: PingerFormProps) {
const { control, handleSubmit, errors } = useForm<FormSchema>({
const editingExistingPinger = !!props.pinger;
const { control, handleSubmit, errors } = useForm<PingerSchema>({
resolver: yupResolver(schema),
defaultValues: props.pinger,
});
const [priceType, setPriceType] = useState<PRICE_TYPE>(DEFAULT_PRICE_TYPE);

Expand All @@ -91,6 +90,7 @@ export default function PingerForm(props: PingerFormProps) {
warning={!!props.warning}
success={!!props.success}
onSubmit={handleSubmit(props.onSubmit)}
data-testid={"pinger-form"}
>
<Controller
name="email"
Expand Down Expand Up @@ -397,29 +397,30 @@ export default function PingerForm(props: PingerFormProps) {
/>
)}
/>

<Controller
name="marketing"
control={control}
defaultValue={false}
render={(props) => (
<Form.Checkbox
inline
id="form-marketing-field"
label={
<label>
Vēlos saņemt mārketinga komunikāciju{" "}
<Label pointing="left">
uzzini pirmais par Brokalys uzlabojumiem!
</Label>
</label>
}
value="agree"
checked={!!props.value}
onChange={() => props.onChange(!props.value)}
/>
)}
/>
{!editingExistingPinger && (
<Controller
name="marketing"
control={control}
defaultValue={false}
render={(props) => (
<Form.Checkbox
inline
id="form-marketing-field"
label={
<label>
Vēlos saņemt mārketinga komunikāciju{" "}
<Label pointing="left">
uzzini pirmais par Brokalys uzlabojumiem!
</Label>
</label>
}
value="agree"
checked={!!props.value}
onChange={() => props.onChange(!props.value)}
/>
)}
/>
)}

{props.error}
{props.warning}
Expand All @@ -433,7 +434,11 @@ export default function PingerForm(props: PingerFormProps) {
primary
type="submit"
role="button"
content="Saņemt nek.īp. paziņojumus"
content={
editingExistingPinger
? "Apstiprināt izmaiņas"
: "Saņemt nek.īp. paziņojumus"
}
formNoValidate
/>
</Grid.Column>
Expand Down
51 changes: 38 additions & 13 deletions src/components/Form/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@ import {

export type PRICE_TYPE = "TOTAL" | "SQM";

export interface FormSchema {
export interface PingerSchema {
id?: string | null;
email: string;
category: "APARTMENT" | "HOUSE" | "LAND";
type: "SELL" | "RENT" | "AUCTION";
price_min: number;
price_max: number;
price_type: PRICE_TYPE;
rooms_min?: number;
rooms_max?: number;
area_m2_min?: number;
area_m2_max?: number;
rooms_min?: number | null;
rooms_max?: number | null;
area_m2_min?: number | null;
area_m2_max?: number | null;
region: string;
privacy_policy: boolean;
frequency: "IMMEDIA" | "DAILY" | "WEEKLY" | "MONTHLY";
frequency: "IMMEDIATE" | "DAILY" | "WEEKLY" | "MONTHLY";
marketing?: boolean;
unsubscribe_key?: string | null;
unsubscribed_at?: number | null;
}

const positiveFormNumber = (): NumberSchema =>
Expand All @@ -35,14 +38,18 @@ const positiveFormNumber = (): NumberSchema =>
String(originalValue).trim() === "" ? undefined : value,
);

const moreThanEqualMin = (min: number | undefined, schema: NumberSchema) => {
const moreThanEqualMin = (
min: number | null | undefined,
schema: NumberSchema<null | undefined | number>,
) => {
if (!min) {
return schema;
}
return schema.min(min);
};

const schema: SchemaOf<FormSchema> = object().shape({
const schema: SchemaOf<PingerSchema> = object().shape({
id: string().uuid().nullable().notRequired(),
email: string().email().required(),
category: mixed().oneOf(["APARTMENT", "HOUSE", "LAND"]).required(),
type: mixed().oneOf(["SELL", "RENT", "AUCTION"]).required(),
Expand All @@ -52,13 +59,29 @@ const schema: SchemaOf<FormSchema> = object().shape({
.when("price_min", moreThanEqualMin)
.max(10000000),
price_type: mixed().oneOf(["TOTAL", "SQM"]).required(),
rooms_min: positiveFormNumber(),
rooms_max: positiveFormNumber().when("rooms_min", moreThanEqualMin).max(20),
area_m2_min: positiveFormNumber(),
rooms_min: positiveFormNumber()
.nullable()
.notRequired()
.transform((v) => (v === null ? undefined : v)),
rooms_max: positiveFormNumber()
.notRequired()
.nullable()
.transform((v) => (v === null ? undefined : v))
.when("rooms_min", moreThanEqualMin)
.max(20),
area_m2_min: positiveFormNumber()
.nullable()
.notRequired()
.transform((v) => (v === null ? undefined : v)),
area_m2_max: positiveFormNumber()
.nullable()
.notRequired()
.transform((v) => (v === null ? undefined : v))
.when("area_m2_min", moreThanEqualMin)
.when("category", (category: string, schema: NumberSchema) =>
schema.max(category === "LAND" ? 1000000 : 1000),
.when(
"category",
(category: string, schema: NumberSchema<null | undefined | number>) =>
schema.max(category === "LAND" ? 1000000 : 1000),
),
region: string()
.required()
Expand All @@ -75,6 +98,8 @@ const schema: SchemaOf<FormSchema> = object().shape({
"Lai izveidotu jaunu PINGERi, ir jāpiekrīt lietošanas noteikumiem un privātuma politikai",
),
marketing: boolean(),
unsubscribe_key: string().nullable().notRequired(),
unsubscribed_at: number().nullable().notRequired(),
});

export default schema;
Loading