Skip to content

Commit

Permalink
feat: add price comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
edvein-rin committed Aug 3, 2024
1 parent b65e61f commit ab8696f
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 9 deletions.
2 changes: 1 addition & 1 deletion docs/kyiv-scooters/.obsidian/workspace.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}
],
"direction": "horizontal",
"width": 300
"width": 300.5
},
"right": {
"id": "5bc5eb9b5cbb16af",
Expand Down
5 changes: 2 additions & 3 deletions docs/kyiv-scooters/Comparison.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
Price was checked on Friday so it may be different the other days.

| | Bolt | Jet | BikeNow | FlyGo | Zelectra | Bird | E-wings |
| --------------------- | --------- | --------- | --------- | --------- | --------- | --------- | ------- |
| **Unlock fee** | 9.00₴ | 10.00₴ | 9.00₴ | 9.00₴ | 9.00₴ | 10.00₴ | |
| **Ride (Weekday)** | 4.10₴/min | 4.25₴/min | 3.50₴/min | 3.50₴/min | 4.25₴/min | 7.00₴/min | |
| **Ride (Weekend)** | | | | | | | |
| **Ride (Weekend)** | 4.10₴/min | 3.9₴/min | 3.50₴/min | 3.50₴/min | 4.25₴/min | 7.00₴/min | |
| **Pause** | 4.10₴/min | 4.25₴/min | | 1.00₴/min | 4.25₴/min | | |
| **Reservation Time** | 3 | 5 | 10 || 5 | | |
| **Reservation Price** | Free | Free | Free | 1.00₴/min | Free | | |
| **Daily cap** | 490₴ | | | | | | |
| **Daily cap** | 490₴ | | | | 499₴ | | |
| **Max Speed** | 20 km/h | 25 km/h | 20 km/h | | | | |
32 changes: 27 additions & 5 deletions src/components/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
export const App = () => (
<div className="flex flex-col gap-2">
<header>Header</header>
<main className="App">Hi there! I'm testing Tailwind setup :)</main>
</div>
import { FC } from "react";

import { Header } from "components/header";

import { IntroSection } from "./ui";
import { PriceComparisonSection } from "./ui/PriceComparisonSection";
import { ANCHORS } from "utils";

export const App: FC = () => (
<>
<Header />
<main>
<IntroSection />
<div
id={ANCHORS.coverageMap}
className="bg-gray-400 h-[512px] flex items-center justify-center"
>
*Мапа покриття*
</div>
<PriceComparisonSection />
</main>
<footer className="flex items-center justify-center h-16 border-t">
<span>
Всі дані на сайті актуальні на <b>03.08.2024</b>.
</span>
</footer>
</>
);
40 changes: 40 additions & 0 deletions src/components/app/ui/IntroSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FC } from "react";

import { scooterProviders } from "data/scooterProviders";
import { Link } from "components/link";
import { TextSection } from "components/text-section";

export const IntroSection: FC = () => (
<TextSection>
<h1>Порівняння електросамокатів Києва</h1>
<p>
<span>
В Києві є <b>{scooterProviders.length}</b> провайдерів електросамокатів
(насправді є ще <b>E-wings</b>, але його потестити не вдалося):{" "}
<ul>
{scooterProviders.map((scooterProvider, index) => (
<li>
<b>{scooterProvider.title}</b> (
<Link
href={scooterProvider.androidAppDownloadUrl}
target="_blank"
rel="noopener noreferrer nofollow"
>
Android
</Link>
,{" "}
<Link
href={scooterProvider.iosAppDownloadUrl}
target="_blank"
rel="noopener noreferrer nofollow"
>
iOS
</Link>
){index === scooterProviders.length - 1 ? "." : ","}
</li>
))}
</ul>
</span>
</p>
</TextSection>
);
31 changes: 31 additions & 0 deletions src/components/app/ui/PriceComparisonSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FC } from "react";

import { ANCHORS } from "utils";
import { TextSection } from "components/text-section";
import { PriceComparisonTable } from "components/price-comparison-table";
import { Link } from "components/link";

export const PriceComparisonSection: FC = () => (
<div className="pb-8">
<TextSection id={ANCHORS.priceComparison}>
<h1>Порівняння цін</h1>
<p>
Деякі дані відсутні (порожні комірки). Якщо маєте актуальну інформацію — можете повідомити
мене в Telegram{" "}
<Link
href="https://t.me/edrickedorty"
target="_blank"
rel="noopener noreferrer nofollow"
>
@EdrickEdorty
</Link>
.{" "}
</p>
</TextSection>
<div className="flex items-center justify-center">
<div className="overflow-auto">
<PriceComparisonTable className="mx-6" />
</div>
</div>
</div>
);
1 change: 1 addition & 0 deletions src/components/app/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./IntroSection";
17 changes: 17 additions & 0 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FC } from "react";

import { Navigation } from "components/navigation";
import { styles } from "utils";

export const Header: FC = () => (
<header className="border h-16 flex justify-center">
<div
className={styles(
"container max-w-[680px] h-full mx-6",
"flex items-center"
)}
>
<Navigation />
</div>
</header>
);
13 changes: 13 additions & 0 deletions src/components/link/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AnchorHTMLAttributes, FC } from "react";
import { styles } from "utils";

export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {}

export const Link: FC<LinkProps> = ({ className, children, ...restProps }) => (
<a
className={styles("underline hover:no-underline", className)}
{...restProps}
>
{children}
</a>
);
21 changes: 21 additions & 0 deletions src/components/navigation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Link } from "components/link";
import { ANCHORS } from "utils";

export const NAV_ITEMS = [
{
title: "Покриття",
route: `#${ANCHORS.coverageMap}`,
},
{
title: "Ціни",
route: `#${ANCHORS.priceComparison}`,
},
];

export const Navigation = () => (
<nav className="flex gap-4 md:gap-8 align-center items-center">
{NAV_ITEMS.map(({ title, route }) => (
<Link href={route}>{title}</Link>
))}
</nav>
);
91 changes: 91 additions & 0 deletions src/components/price-comparison-table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { FC } from "react";

import { scooterProviders } from "data/scooterProviders";
import { styles } from "utils";

import { Row } from "./ui";

export type PriceComparisonTableProps = {
className?: string;
};

export const PriceComparisonTable: FC<PriceComparisonTableProps> = ({
className,
}) => (
<table
align="left"
className={styles(
"[&>tr:not(:first-child)>th]:text-left",
"[&>tr>td]:text-right",
"[&_th]:px-4 [&_th]:py-1.5",
"[&_td]:px-4 [&_td]:py-1.5",
"[&_td]:border-b [&_th]:border-b",
className
)}
>
<tr className="[&>th]:text-right">
<th>{""}</th>
{scooterProviders.map((scooterProvider) => (
<th>{scooterProvider.title}</th>
))}
</tr>
<Row
label="Розблокування"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.unlockPrice
)}
highlight="min"
formatValue={(value) => value + " грн"}
/>
<Row
label="Будні"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.rideWeekdayPricePerMinute
)}
highlight="min"
formatValue={(value) => value + " грн/хв"}
/>
<Row
label="Вихідні"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.rideWeekendPricePerMinute
)}
highlight="min"
formatValue={(value) => value + " грн/хв"}
/>
<Row
label="Пауза"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.pausePricePerMinute
)}
highlight="min"
formatValue={(value) => value + " грн/хв"}
/>
<Row
label="Бронювання"
values={scooterProviders.map((scooterProvider) =>
scooterProvider.reservationPricePerMinute === undefined &&
scooterProvider.reservationTimeInMinutes === undefined
? undefined
: scooterProvider.reservationPricePerMinute === null
? `${scooterProvider.reservationTimeInMinutes} хв`
: scooterProvider.reservationPricePerMinute + " грн/хв"
)}
/>
<Row
label="Ліміт на день"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.dailyCap
)}
formatValue={(value) => (value === null ? "—" : value + " грн")}
/>
<Row
label="Максимальна швидкість"
values={scooterProviders.map(
(scooterProvider) => scooterProvider.maxSpeedKmPerHour
)}
formatValue={(value) => value + " км/г"}
highlight="max"
/>
</table>
);
67 changes: 67 additions & 0 deletions src/components/price-comparison-table/ui/Row/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { FC } from "react";
import { styles } from "utils";

export const findSmallest = (values: Value[]): Value =>
values.reduce<Value>((previousMinValue, value) => {
if (typeof value === "string" || typeof previousMinValue === "string")
return previousMinValue;
if (value === null || previousMinValue === null) return null;
if (value === undefined) return previousMinValue;
if (previousMinValue === undefined) return value;

return value < previousMinValue ? value : previousMinValue;
}, undefined);

export const findBiggest = (values: Value[]): Value =>
values.reduce<Value>((previousMinValue, value) => {
if (typeof value === "string" || typeof previousMinValue === "string")
return previousMinValue;
if (value === null || previousMinValue === null) return null;
if (value === undefined) return previousMinValue;
if (previousMinValue === undefined) return value;

return value > previousMinValue ? value : previousMinValue;
}, undefined);

export type Value = string | number | null | undefined;

export type RowProps = {
label: string;
values: Value[];
highlight?: "min" | "max" | undefined;
formatValue?: (value: NonNullable<Value> | null) => string;
};

export const Row: FC<RowProps> = ({
label,
values,
highlight,
formatValue = (value) => value,
}) => {
const valueToHighlight =
highlight === undefined
? undefined
: highlight === "min"
? findSmallest(values)
: findBiggest(values);

return (
<tr>
<th>{label}</th>
{values.map((value) => {
const highlighted =
value === valueToHighlight && valueToHighlight !== undefined;
return (
<td
className={styles(
"whitespace-nowrap",
highlighted && "font-semibold"
)}
>
{value === undefined ? null : formatValue(value)}
</td>
);
})}
</tr>
);
};
1 change: 1 addition & 0 deletions src/components/price-comparison-table/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Row";
14 changes: 14 additions & 0 deletions src/components/text-section/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FC, ReactNode } from "react";

export type TextSectionProps = {
id?: string;
children?: ReactNode;
};

export const TextSection: FC<TextSectionProps> = ({ id, children }) => (
<section id={id} className="flex justify-center">
<div className="max-w-[680px] mx-6 w-full min-w-0 pt-8 pb-16 prose md:prose-lg">
{children}
</div>
</section>
);
Loading

0 comments on commit ab8696f

Please sign in to comment.