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

[Feature] Implement Client Routing #631

Merged
merged 13 commits into from
Mar 30, 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
5 changes: 4 additions & 1 deletion packages/ui/src/components/date-input/date-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import DateInput from "./date-input";
describe("Date Input", () => {
const user = userEvent.setup();
beforeEach(async () => {
const onSelectMock = jest.fn();
render(
<div>
<DateInput id="From">From</DateInput>
<DateInput id="From" onSelect={onSelectMock}>
From
</DateInput>
<button data-testid="outside-event-listener">Exit calendar</button>
</div>,
);
Expand Down
18 changes: 15 additions & 3 deletions packages/ui/src/components/date-input/date-input.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { onEvent, Nil } from "@lucky-parking/typings";
import { formatToMiddleEndian } from "@lucky-parking/utilities/dist/date";
import clsx from "clsx";
import { PropsWithChildren, useEffect, useRef, useState } from "react";
Expand All @@ -6,14 +7,20 @@ import Calendar from "../calendar";
interface DateInputProps {
id: string;
children: PropsWithChildren;
date?: Date | Nil;
onSelect: onEvent;
}

export default function DateInput({ children, ...props }: PropsWithChildren<DateInputProps>) {
const { id } = props;
const { id, date, onSelect } = props;
const calendarRef = useRef<HTMLDivElement>(null);
const [value, setValue] = useState<string | null>();
const [value, setValue] = useState<string | Nil>(date ? formatToMiddleEndian(date) : null);
const [isCalendarVisible, setCalendarVisible] = useState(false);

useEffect(() => {
setValue(date ? formatToMiddleEndian(date) : null);
}, [date]);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (!calendarRef || calendarRef.current === null) return;
Expand All @@ -27,6 +34,11 @@ export default function DateInput({ children, ...props }: PropsWithChildren<Date
};
}, [calendarRef, setValue]);

const handleSelectValueChange = (newValue: Date) => {
setValue(formatToMiddleEndian(newValue));
onSelect({ id: id, date: newValue });
};

return (
<div className="relative flex-auto">
<div
Expand All @@ -51,7 +63,7 @@ export default function DateInput({ children, ...props }: PropsWithChildren<Date
</div>
{isCalendarVisible && (
<div className="absolute z-50 mt-2 drop-shadow" ref={calendarRef}>
<Calendar onSelectValueChange={(newValue: Date) => setValue(formatToMiddleEndian(newValue))} />
<Calendar onSelectValueChange={handleSelectValueChange} />
</div>
)}
</div>
Expand Down
29 changes: 17 additions & 12 deletions packages/website/src/entities/branding/ui/logo.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import BrandMark from "./brandmark.svg";
import { Link } from "react-router-dom";
import type { onEvent } from "@lucky-parking/typings";

type LogoProps = {
onClick?: onEvent;
};

export default function Logo(props: LogoProps) {
const { onClick } = props;

export default function Logo() {
return (
/**
* reloadDocument - A property used in the Link component from React Router to skip client side routing and let the browser handle the transition normally (as if it were an <a href>).
* @typedef {boolean} reloadDocument
*/
<Link to="/" reloadDocument={true} className="flex items-center space-x-2">
<img src={BrandMark} alt="Parking Insights brand mark" />
<div className="flex items-center space-x-2" onClick={onClick}>
<Link to="/" reloadDocument={true} className="flex items-center space-x-2">
<img src={BrandMark} alt="Parking Insights brand mark" />

<div className="select-none -space-y-1 font-semibold uppercase text-blue-600">
<p>Parking</p>
<p>Insights</p>
</div>
</Link>
<div className="select-none -space-y-1 font-semibold uppercase text-blue-600">
<p>Parking</p>
<p>Insights</p>
</div>
</Link>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { CitationDataCategories } from "../lib";

interface CitationDataCategorySelectionProps {
onSelect: onEvent;
category?: string;
}

const CITATION_DATA_CATEGORIES = _.map(CitationDataCategories, (value) => ({ value, text: value }));

export default function CitationDataCategorySelection(props: CitationDataCategorySelectionProps) {
const { onSelect } = props;
const { onSelect, category } = props;

return (
<PickList
Expand All @@ -19,6 +20,7 @@ export default function CitationDataCategorySelection(props: CitationDataCategor
onChange={onSelect}
options={CITATION_DATA_CATEGORIES}
placeholder={CITATION_DATA_CATEGORIES[0].text}
value={category}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import CitationDataFilter from "./citation-data-filter";
describe("Citation-data-filter", () => {
describe("rendering", () => {
beforeEach(() => {
render(<CitationDataFilter onCategorySelect={() => {}} onDatePresetSelect={() => {}} />);
render(
<CitationDataFilter onCategorySelect={() => {}} onDatePresetSelect={() => {}} onCustomDateSelect={() => {}} />,
);
});

it("renders citation data categories with initial placeholder", async () => {
Expand All @@ -27,11 +29,16 @@ describe("Citation-data-filter", () => {
describe("interactions", () => {
const onCategorySelectMock = jest.fn();
const onDatePresetSelectMock = jest.fn();
const onCustomDateSelectMock = jest.fn();

beforeEach(async () => {
const user = userEvent.setup();
render(
<CitationDataFilter onCategorySelect={onCategorySelectMock} onDatePresetSelect={onDatePresetSelectMock} />,
<CitationDataFilter
onCategorySelect={onCategorySelectMock}
onDatePresetSelect={onDatePresetSelectMock}
onCustomDateSelect={onCustomDateSelectMock}
/>,
);

await user.click(screen.getByRole("combobox", { name: "citation-data-categories" }));
Expand Down
32 changes: 23 additions & 9 deletions packages/website/src/features/citation/ui/citation-data-filter.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
import type { onEvent } from "@lucky-parking/typings";
import type { onEvent, Nil } from "@lucky-parking/typings";
import DateInput from "@lucky-parking/ui/src/components/date-input";
import CitationDataCategorySelection from "./citation-data-category-selection";
import CitationDatePresetsSelection from "./citation-date-presets-selection";

interface CitationDataFilterProps {
onCategorySelect: onEvent;
onDatePresetSelect: onEvent;
onCustomDateSelect: onEvent;
category?: string;
datePreset?: string;
customDateFromInput?: Date | Nil;
customDateToInput?: Date | Nil;
}

export default function CitationDataFilter(props: CitationDataFilterProps) {
const { onCategorySelect, onDatePresetSelect } = props;
const {
onCategorySelect,
onDatePresetSelect,
onCustomDateSelect,
category,
datePreset,
customDateFromInput,
customDateToInput,
} = props;

return (
<>
<CitationDataCategorySelection onSelect={onCategorySelect} />

<CitationDataCategorySelection category={category} onSelect={onCategorySelect} />
<div className="flex w-full items-center space-x-2">
<CitationDatePresetsSelection onSelect={onDatePresetSelect} />

<CitationDatePresetsSelection datePreset={datePreset} onSelect={onDatePresetSelect} />
<p className="paragraph-2 text-black-400 font-medium">or</p>

<div className="flex flex-auto items-center space-x-1">
<DateInput id="From">From</DateInput>
<DateInput id="To">To</DateInput>
<DateInput id="From" date={customDateFromInput} onSelect={onCustomDateSelect}>
From
</DateInput>
<DateInput id="To" date={customDateToInput} onSelect={onCustomDateSelect}>
To
</DateInput>
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { RelativeDatePresets } from "@/shared/lib/constants/date";

interface CitationDataPresetsSelection {
onSelect: onEvent;
datePreset?: string;
}

const CITATION_DATE_PRESETS = _.map(RelativeDatePresets, (value) => ({ value, text: value }));

export default function CitationDatePresetsSelection(props: CitationDataPresetsSelection) {
const { onSelect } = props;
const { onSelect, datePreset } = props;

return (
<PickList
Expand All @@ -19,6 +20,7 @@ export default function CitationDatePresetsSelection(props: CitationDataPresetsS
onChange={onSelect}
options={CITATION_DATE_PRESETS}
placeholder={CITATION_DATE_PRESETS[0].text}
value={datePreset}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useSearchParams } from "react-router-dom";
import { RelativeDatePresets } from "@/shared/lib/constants/date";
import { formatToMiddleEndian } from "@lucky-parking/utilities/dist/date";
import { calculateDateRange } from "@/shared/lib/utilities/date";

export default function useCitationSearchParams() {
const [searchParams, setSearchParams] = useSearchParams();

const getCategory = () => searchParams.get("category");
const setCategory = (value: string) =>
setSearchParams((prevParams) => {
prevParams.set("category", value);
return prevParams;
});

const getDatePreset = () => searchParams.get("date_preset");
const setDatePreset = (value: RelativeDatePresets) =>
setSearchParams((prevParams) => {
prevParams.set("date_preset", value);
return prevParams;
});

const getDateRange = () => {
const dateFromSearchQuery = searchParams.get("date_from");
const dateToSearchQuery = searchParams.get("date_to");

if (dateFromSearchQuery && dateToSearchQuery) {
const dateFrom = new Date(dateFromSearchQuery);
const dateTo = new Date(dateToSearchQuery);
return [dateFrom, dateTo];
} else {
return null;
}
};
const setDateRange = (value: RelativeDatePresets | Date[]) =>
setSearchParams((prevParams) => {
let dateRange: Date[] = [];
if (Array.isArray(value)) {
dateRange = value;
} else {
dateRange = calculateDateRange(value);
}
prevParams.set("date_from", formatToMiddleEndian(dateRange[0]));
prevParams.set("date_to", formatToMiddleEndian(dateRange[1]));
return prevParams;
});

const getPlaceName = () => searchParams.get("place_name");
const setPlaceName = (value: string) =>
setSearchParams((prevParams) => {
prevParams.set("place_name", value);
return prevParams;
});

const getPlaceType = () => searchParams.get("place_type");
const setPlaceType = (value: string) =>
setSearchParams((prevParams) => {
prevParams.set("place_type", value);
return prevParams;
});

const getRegion1 = () => searchParams.get("region1");
const setRegion1 = (value: string) =>
setSearchParams((prevParams) => {
prevParams.set("region1", value);
return prevParams;
});
const deleteRegion1 = () =>
setSearchParams((prevParams) => {
prevParams.delete("region1");
return prevParams;
});

const getRegion2 = () => searchParams.get("region2");
const setRegion2 = (value: string) =>
setSearchParams((prevParams) => {
prevParams.set("region2", value);
return prevParams;
});
const deleteRegion2 = () =>
setSearchParams((prevParams) => {
prevParams.delete("region2");
return prevParams;
});

const getCompareMode = () => searchParams.get("compare_mode");
const setCompareMode = () =>
setSearchParams((prevParams) => {
prevParams.set("compare_mode", "true");
return prevParams;
});

const getVisualizationMode = () => searchParams.get("visualization_mode");
const setVisualizationMode = () =>
setSearchParams((prevParams) => {
prevParams.set("visualization_mode", "true");
return prevParams;
});
const deleteVisualizationMode = () =>
setSearchParams((prevParams) => {
prevParams.delete("visualization_mode");
return prevParams;
});

const clearSearchParams = () => setSearchParams({});

return {
category: {
get: getCategory,
set: setCategory,
},
datePreset: {
get: getDatePreset,
set: setDatePreset,
},
dateRange: {
get: getDateRange,
set: setDateRange,
},
placeName: {
get: getPlaceName,
set: setPlaceName,
},
placeType: {
get: getPlaceType,
set: setPlaceType,
},
region1: {
get: getRegion1,
set: setRegion1,
delete: deleteRegion1,
},
region2: {
get: getRegion2,
set: setRegion2,
delete: deleteRegion2,
},
compareMode: {
get: getCompareMode,
set: setCompareMode,
},
visualizationMode: {
get: getVisualizationMode,
set: setVisualizationMode,
delete: deleteVisualizationMode,
},
clearSearchParams: clearSearchParams,
searchParams: searchParams,
};
}
Loading
Loading