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

[Frontend] Owner 画面(椅子一覧、売上)、Client 画面モーダルに値段表示 #244

Merged
merged 16 commits into from
Nov 19, 2024
Merged
14 changes: 14 additions & 0 deletions frontend/app/components/modules/list/list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ComponentProps, PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";

export function ListItem({
children,
className,
...props
}: PropsWithChildren<ComponentProps<"li">>) {
return (
<li {...props} className={twMerge("px-4 py-3 border-b", className)}>
{children}
</li>
);
}
13 changes: 13 additions & 0 deletions frontend/app/components/modules/list/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ComponentProps, PropsWithChildren } from "react";

export function List({
children,
className,
...props
}: PropsWithChildren<ComponentProps<"ul">>) {
return (
<ul {...props} className={className}>
{children}
</ul>
);
}
14 changes: 14 additions & 0 deletions frontend/app/components/modules/owner-header/owner-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FC } from "react";
import { useClientProviderContext } from "~/contexts/provider-context";

export const OwnerHeader: FC = () => {
const { provider } = useClientProviderContext();

return (
<div className="flex items-center my-8 px-6">
{/* TODO: UI */}
<div className="border rounded-full size-16"></div>
<span className="text-2xl ms-4">{provider?.name}</span>
</div>
);
};
15 changes: 15 additions & 0 deletions frontend/app/components/modules/price-text/price-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentPropsWithoutRef, FC } from "react";
import { Text } from "~/components/primitives/text/text";

type PriceTextProps = Omit<
ComponentPropsWithoutRef<typeof Text>,
"children"
> & {
value: number;
};

const formatter = new Intl.NumberFormat("ja-JP");

export const PriceText: FC<PriceTextProps> = ({ value, ...rest }) => {
return <Text {...rest}>{formatter.format(value)} 円</Text>;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら、よき感じで cherry-pick いただけるとありがたいです 🙏
058882b

9 changes: 9 additions & 0 deletions frontend/app/components/primitives/badge/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PropsWithChildren } from "react";

export const Badge = ({ children }: PropsWithChildren) => {
return (
<span className="border rounded-md border-gray-300 text-sm px-2 py-1">
{children}
</span>
);
};
20 changes: 17 additions & 3 deletions frontend/app/components/primitives/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ComponentProps, FC, PropsWithChildren, useMemo } from "react";
import { twMerge } from "tailwind-merge";

type Variant = "light" | "primary" | "danger";
type Size = "sm" | "md";

export const ButtonLink: FC<PropsWithChildren<ComponentProps<typeof Link>>> = ({
to,
Expand All @@ -25,8 +26,10 @@ export const ButtonLink: FC<PropsWithChildren<ComponentProps<typeof Link>>> = ({
};

export const Button: FC<
PropsWithChildren<ComponentProps<"button"> & { variant?: Variant }>
> = ({ children, className, variant, ...props }) => {
PropsWithChildren<
ComponentProps<"button"> & { variant?: Variant; size?: Size }
>
> = ({ children, className, variant, size = "md", ...props }) => {
const variantClasses = useMemo(() => {
switch (variant) {
case "primary":
Expand All @@ -38,12 +41,23 @@ export const Button: FC<
return "bg-[#F0EFED] active:brightness-90 hover:brightness-90 focus:brightness-90";
}
}, [variant]);

const sizeClasses = useMemo(() => {
switch (size) {
case "sm":
return "py-2 px-3";
case "md":
return "py-3 px-4";
}
}, [size]);

return (
<button
type="button"
className={twMerge(
"rounded-md bg-neutral-800 py-3 px-4 border border-transparent text-center text-sm transition-all shadow-md hover:shadow-lg disabled:opacity-50 disabled:shadow-none ml-2",
"rounded-md bg-neutral-800 border border-transparent text-center text-sm transition-all shadow-md hover:shadow-lg disabled:opacity-50 disabled:shadow-none ml-2",
variantClasses,
sizeClasses,
className,
)}
{...props}
Expand Down
21 changes: 21 additions & 0 deletions frontend/app/components/primitives/chair-model/chair-model.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FC } from "react";
import { twMerge } from "tailwind-merge";
import { CarGreenIcon } from "~/components/icon/car-green";
import { CarRedIcon } from "~/components/icon/car-red";
import { CarYellowIcon } from "~/components/icon/car-yellow";

<CarRedIcon className="size-[76px] mb-4" />;

export const ChairModel: FC<{ model: string; className?: string }> = (
props,
) => {
const Chair = (() => {
// TODO: 仮実装
const model = props.model;
if (/^[a-n]/i.test(model)) return CarGreenIcon;
if (/^[o-z]/i.test(model)) return CarYellowIcon;
return CarRedIcon;
})();

return <Chair className={twMerge(["size-[1.5rem]", props.className])} />;
};
26 changes: 26 additions & 0 deletions frontend/app/components/primitives/form/date.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FC, PropsWithoutRef } from "react";
import { twMerge } from "tailwind-merge";

type DateInputProps = PropsWithoutRef<{
id: string;
name: string;
className?: string;
required?: boolean;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}>;

export const DateInput: FC<DateInputProps> = (props) => {
return (
<input
type="date"
id={props.id}
name={props.name}
className={twMerge(
"mt-1 p-2 w-full border border-neutral-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
props?.className,
)}
required={props.required}
onChange={props.onChange}
></input>
);
};
2 changes: 1 addition & 1 deletion frontend/app/components/primitives/tab/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Tab = <T extends string>({
onTabClick,
}: TextProps<T>) => {
return (
<nav className="border-b mb-4">
<nav className="border-b">
<ul className="flex">
{tabs.map((tab) => (
<li
Expand Down
13 changes: 7 additions & 6 deletions frontend/app/components/primitives/text/text.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FC, PropsWithChildren } from "react";
import { twMerge } from "tailwind-merge";

type Size = "xl" | "lg" | "sm" | "xs";
type Size = "2xl" | "xl" | "lg" | "sm" | "xs";

type Variant = "danger";

type TextProps = PropsWithChildren<{
tagName?: "p" | "div";
tagName?: "p" | "div" | "span";
bold?: boolean;
size?: Size;
variant?: Variant;
Expand All @@ -14,6 +15,8 @@ type TextProps = PropsWithChildren<{

const getSizeClass = (size?: Size) => {
switch (size) {
case "2xl":
return "text-2xl";
case "xl":
return "text-xl";
case "lg":
Expand Down Expand Up @@ -47,14 +50,12 @@ export const Text: FC<TextProps> = ({
const Tag = tagName;
return (
<Tag
className={[
className={twMerge([
bold ? "font-bold" : "",
getSizeClass(size),
getVariantClass(variant),
className,
]
.filter(Boolean)
.join(" ")}
])}
>
{children}
</Tag>
Expand Down
54 changes: 31 additions & 23 deletions frontend/app/contexts/provider-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import {
type ReactNode,
} from "react";
import {
OwnerGetChairsResponse,
OwnerGetSalesResponse,
fetchOwnerGetChairs,
fetchOwnerGetSales,
} from "~/apiClient/apiComponents";

type ProviderChair = { id: string; name: string };

type ClientProviderRequest = Partial<{
chairs: ProviderChair[];
chairs: OwnerGetChairsResponse["chairs"];
sales: OwnerGetSalesResponse;
provider: {
id?: string;
provider?: {
id: string;
name: string;
};
}>;

Expand All @@ -36,11 +37,14 @@ const DUMMY_DATA = {

const ClientProviderContext = createContext<Partial<ClientProviderRequest>>({});

const timestamp = (date: string) => Math.floor(new Date(date).getTime() / 1000);

export const ProviderProvider = ({ children }: { children: ReactNode }) => {
// TODO:
const [searchParams] = useSearchParams();

const id = searchParams.get("id") ?? undefined;
const name = searchParams.get("name") ?? undefined;
const since = searchParams.get("since") ?? undefined;
const until = searchParams.get("until") ?? undefined;

Expand All @@ -56,6 +60,7 @@ export const ProviderProvider = ({ children }: { children: ReactNode }) => {
}
}, []);

const [chairs, setChairs] = useState<OwnerGetChairsResponse>();
const [sales, setSales] = useState<OwnerGetSalesResponse>();

useEffect(() => {
Expand All @@ -67,14 +72,22 @@ export const ProviderProvider = ({ children }: { children: ReactNode }) => {
});
} else {
const abortController = new AbortController();
(async () => {
setSales(
await fetchOwnerGetSales(
{ queryParams: { since: Number(since), until: Number(until) } },
abortController.signal,
),
);
})().catch((reason) => {
Promise.all([
fetchOwnerGetChairs({}, abortController.signal).then((res) =>
setChairs(res),
),
since && until
? fetchOwnerGetSales(
{
queryParams: {
since: timestamp(since),
until: timestamp(until),
},
},
abortController.signal,
).then((res) => setSales(res))
: Promise.resolve(),
]).catch((reason) => {
if (typeof reason === "string") {
console.error(`CONSOLE PROMISE ERROR: ${reason}`);
}
Expand All @@ -83,20 +96,15 @@ export const ProviderProvider = ({ children }: { children: ReactNode }) => {
abortController.abort();
};
}
}, [setSales, since, until, isDummy]);
}, [setChairs, setSales, since, until, isDummy]);

const responseClientProvider = useMemo<ClientProviderRequest>(() => {
return {
chairs: chairs?.chairs ?? [],
sales,
chairs: sales?.chairs?.map((chair) => ({
id: chair.id,
name: chair.name,
})),
provider: {
id,
},
} satisfies ClientProviderRequest;
}, [sales, id]);
provider: id && name ? { id, name } : undefined,
};
}, [chairs, sales, id, name]);

return (
<ClientProviderContext.Provider value={responseClientProvider}>
Expand Down
11 changes: 10 additions & 1 deletion frontend/app/routes/client._index/driving-state/carrying.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { FC } from "react";
import { CarGreenIcon } from "~/components/icon/car-green";
import { LocationButton } from "~/components/modules/location-button/location-button";
import { PriceText } from "~/components/modules/price-text/price-text";
import { Text } from "~/components/primitives/text/text";
import { Coordinate } from "~/types";

export const Carrying: FC<{ destLocation?: Coordinate }> = ({
export const Carrying: FC<{ destLocation?: Coordinate; fare?: number }> = ({
destLocation,
fare,
}) => {
return (
<div className="w-full h-full px-8 flex flex-col items-center justify-center">
Expand All @@ -16,6 +18,13 @@ export const Carrying: FC<{ destLocation?: Coordinate }> = ({
<LocationButton label="現在地" className="w-80" />
<Text size="xl">↓</Text>
<LocationButton label="目的地" location={destLocation} className="w-80" />
<p className="mt-8">
{typeof fare === "number" ? (
<>
予定運賃: <PriceText tagName="span" value={fare} />
</>
) : null}
</p>
</div>
);
};
11 changes: 10 additions & 1 deletion frontend/app/routes/client._index/driving-state/dispatched.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { FC } from "react";
import { CarYellowIcon } from "~/components/icon/car-yellow";
import { LocationButton } from "~/components/modules/location-button/location-button";
import { PriceText } from "~/components/modules/price-text/price-text";
import { Text } from "~/components/primitives/text/text";
import { Coordinate } from "~/types";

export const Dispatched: FC<{ destLocation?: Coordinate }> = ({
export const Dispatched: FC<{ destLocation?: Coordinate; fare?: number }> = ({
destLocation,
fare,
}) => {
return (
<div className="w-full h-full px-8 flex flex-col items-center justify-center">
Expand All @@ -16,6 +18,13 @@ export const Dispatched: FC<{ destLocation?: Coordinate }> = ({
<LocationButton label="現在地" className="w-80" />
<Text size="xl">↓</Text>
<LocationButton label="目的地" location={destLocation} className="w-80" />
<p className="mt-8">
{typeof fare === "number" ? (
<>
予定運賃: <PriceText tagName="span" value={fare} />
</>
) : null}
</p>
</div>
);
};
Loading