Skip to content

Commit

Permalink
feat[STK-33]: create stack frequency section (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
berteotti authored Aug 1, 2023
1 parent 34bb9fe commit 016be7c
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 36 deletions.
8 changes: 7 additions & 1 deletion packages/app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ export const metadata: Metadata = {
metadataBase: new URL(process.env.STACKLY_URL ?? defaultStacklyUrl),
title: "Stackly | Stack crypto over time.",
description:
"Stackly is a simple, non-custodial tool that uses the CoW protocol to place recurring swaps based on DCA.."
"Stackly is a simple, non-custodial tool that uses the CoW protocol to place recurring swaps based on DCA.",
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
};

export default function RootLayout({ children }: PropsWithChildren) {
Expand Down
4 changes: 3 additions & 1 deletion packages/app/app/ui/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
HeadingText,
OverlineText,
DialogContent,
CaptionText,
Calendar,
} from "@/ui";
import { CaptionText } from "@/ui/text/CaptionText";
import { useRef, useState } from "react";
import { DatePicker } from "../../components/DatePicker";

export default function Page() {
// radioButtons
Expand Down
170 changes: 170 additions & 0 deletions packages/app/components/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"use client";

import { format, isToday, isTomorrow } from "date-fns";

import { Button, Calendar, Icon } from "@/ui";
import { Popover } from "@headlessui/react";

import { Dispatch, Ref, SetStateAction, useEffect, useState } from "react";
import { usePopper } from "react-popper";
import { twMerge } from "tailwind-merge";

interface DatePickerProps {
dateTime: Date;
setDateTime: Dispatch<SetStateAction<Date>>;
className?: string;
timeCaption: string;
fromDate?: Date;
}

export function DatePicker({
dateTime,
setDateTime,
className,
timeCaption,
fromDate,
}: DatePickerProps) {
const [currentDate, setCurrentDate] = useState<Date>(new Date(dateTime));
const [hours, setHours] = useState(format(currentDate, "HH"));
const [minutes, setMinutes] = useState(format(currentDate, "mm"));

const [referenceElement, setReferenceElement] = useState<
HTMLButtonElement | undefined
>();
const [popperElement, setPopperElement] = useState<
HTMLDivElement | undefined
>();

const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom",
});

const formattedDate = () => {
if (isToday(dateTime)) return format(dateTime, "'Today at' HH:mm");
if (isTomorrow(dateTime)) return format(dateTime, "'Tomorrow at' HH:mm");

return format(dateTime, "dd MMM Y 'at' HH:mm");
};

useEffect(() => {
const newDate = new Date(dateTime);
setCurrentDate(newDate);
setHours(format(newDate, "HH"));
setMinutes(format(newDate, "mm"));
}, [dateTime]);

return (
<Popover>
{({ open }) => (
<>
<Popover.Button
ref={setReferenceElement as Ref<HTMLButtonElement>}
className={twMerge(
"flex justify-between items-center focus:border-0",
className
)}
>
<span>{formattedDate()}</span>
<Icon name="caret-down" className="mr-2 h-4 w-4 text-black" />
</Popover.Button>
{open && (
<>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<Popover.Panel
ref={setPopperElement as Ref<HTMLDivElement>}
style={styles.popper}
{...attributes.popper}
className="w-fit h-fit m-auto max-md:!fixed max-md:!inset-0 max-md:!flex max-md:!items-center max-md:!justify-center max-md:!transform" /* This styles will make it hover on mobile */
>
{({ close }) => (
<div className="flex flex-col space-y-2 bg-white divide-y divide-surface-50 rounded-2xl border border-surface-50">
<Calendar
mode="single"
selected={currentDate}
defaultMonth={currentDate}
onSelect={(newDate: Date | undefined) => {
if (!newDate) return;
newDate.setHours(currentDate.getHours());
newDate.setMinutes(currentDate.getMinutes());
setCurrentDate(newDate);
}}
initialFocus
className=""
fromDate={fromDate || new Date()}
/>
<div className="flex flex-col space-y-2 p-4">
<div className="flex justify-between items-center">
<span>{timeCaption}</span>
<div className="flex space-x-2 items-center">
<div className="flex items-center border border-surface-75 rounded-xl py-2 px-3 text-em-low">
<input
className="outline-none font-semibold flex-grow text-sm w-5 text-center"
type="number"
pattern="[0-9]*"
value={hours}
onKeyDown={(evt) =>
["e", "E", "+", "-"].includes(evt.key) &&
evt.preventDefault()
}
onChange={(event) => {
const hours = event.target.value;
const hoursNumber = Number(hours);
if (
hoursNumber >= 0 &&
hoursNumber < 24 &&
hours.length < 3
) {
setHours(hours);
}
}}
/>
</div>
<span>:</span>
<div className="flex items-center border border-surface-75 rounded-xl py-2 px-3 text-em-low">
<input
className="outline-none font-semibold flex-grow text-sm w-5 text-center"
type="number"
pattern="[0-9]*"
value={minutes}
onKeyDown={(evt) =>
["e", "E", "+", "-"].includes(evt.key) &&
evt.preventDefault()
}
onChange={(event) => {
const minutes = event.target.value;
const minutesNumber = Number(minutes);
if (
minutesNumber >= 0 &&
minutesNumber < 60 &&
minutes.length < 3
) {
setMinutes(minutes);
}
}}
/>
</div>
</div>
</div>
<Button
action="primary"
onClick={() => {
const newDate = new Date(currentDate);
newDate.setHours(Number(hours));
newDate.setMinutes(Number(minutes));
setDateTime(newDate);
close();
}}
>
Set Date and Time
</Button>
</div>
</div>
)}
</Popover.Panel>
</>
)}
</>
)}
</Popover>
);
}
1 change: 1 addition & 0 deletions packages/app/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import TokenPicker from "./token-picker/TokenPicker";
export * from "./stackbox";
export * from "./FromToStackTokenPair";
export * from "./TokenIcon";
export * from "./DatePicker";

export { ConnectButton, Navbar, SelectNetwork, TokenPicker };
119 changes: 87 additions & 32 deletions packages/app/components/stackbox/Stackbox.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
"use client";

import { useRef, useState } from "react";
import { useRef, useState, useEffect } from "react";
import { cx } from "class-variance-authority";

import { BodyText, Button, Icon, RadioButton, TitleText } from "@/ui";
import { ConfirmStackModal, TokenIcon, TokenPicker } from "@/components";
import {
ConfirmStackModal,
TokenIcon,
TokenPicker,
DatePicker,
} from "@/components";
import { TokenFromTokenlist } from "@/models/token";

interface SelectTokenButtonProps {
Expand All @@ -13,6 +18,28 @@ interface SelectTokenButtonProps {
token?: TokenFromTokenlist;
}

const HOUR_OPTION = "hour";
const DAY_OPTION = "day";
const WEEK_OPTION = "week";
const MONTH_OPTION = "month";

const frequencyOptions = [
{ option: HOUR_OPTION, name: "Hour" },
{ option: DAY_OPTION, name: "Day" },
{ option: WEEK_OPTION, name: "Week" },
{ option: MONTH_OPTION, name: "Month" },
];

const endDateByFrequency: Record<string, number> = {
[HOUR_OPTION]: new Date().setDate(new Date().getDate() + 2),
[DAY_OPTION]: new Date().setMonth(new Date().getMonth() + 1),
[WEEK_OPTION]: new Date().setMonth(new Date().getMonth() + 3),
[MONTH_OPTION]: new Date().setFullYear(new Date().getFullYear() + 1),
};
const startDateTimeTimestamp = new Date().setMinutes(
new Date().getMinutes() + 30
);

export const Stackbox = () => {
const searchTokenBarRef = useRef<HTMLInputElement>(null);
const [isConfirmStackOpen, setConfirmStackIsOpen] = useState(false);
Expand All @@ -21,6 +48,15 @@ export const Stackbox = () => {
const [tokenFrom, setTokenFrom] = useState<TokenFromTokenlist>();
const [tokenTo, setTokenTo] = useState<TokenFromTokenlist>();

const [frequency, setFrequency] = useState<string>(HOUR_OPTION);

const [startDateTime, setStartDateTime] = useState<Date>(
new Date(startDateTimeTimestamp)
);
const [endDateTime, setEndDateTime] = useState<Date>(
new Date(endDateByFrequency[frequency])
);

const closeConfirmStack = () => setConfirmStackIsOpen(false);
const closeTokenPicker = () => setTokenPickerIsOpen(false);
const openConfirmStack = () => setConfirmStackIsOpen(true);
Expand All @@ -30,6 +66,10 @@ export const Stackbox = () => {
};
const selectToken = isPickingTokenFrom ? setTokenFrom : setTokenTo;

useEffect(() => {
setEndDateTime(new Date(endDateByFrequency[frequency]));
}, [frequency]);

return (
<>
<div className="max-w-lg mx-auto my-24 bg-white shadow-2xl rounded-2xl">
Expand Down Expand Up @@ -58,42 +98,57 @@ export const Stackbox = () => {
/>
</div>
</div>
<div className="px-5 py-6">
<div className="px-5 py-6 space-y-6">
<div className="space-y-2">
<TitleText weight="bold" className="text-em-med">
Stack WETH every
</TitleText>
<div className="flex space-x-2">
<RadioButton
name="hour"
id="hour"
checked={true}
value={"0"}
onChange={() => {}}
>
Hour
</RadioButton>
<RadioButton
name="week"
id="week"
checked={false}
value={"1"}
onChange={() => {}}
>
Week
</RadioButton>
<RadioButton
name="month"
id="month"
checked={false}
value={"2"}
onChange={() => {}}
>
Month
</RadioButton>
<div className="space-y-6">
<div className="flex space-x-2">
{frequencyOptions.map(({ option, name }) => {
const isSelected = frequency === option;
return (
<RadioButton
key={option}
name={option}
id={option}
checked={isSelected}
value={option}
onChange={(event) => setFrequency(event.target.value)}
>
<BodyText
size={2}
className={!isSelected ? "text-em-med" : ""}
>
{name}
</BodyText>
</RadioButton>
);
})}
</div>
<div className="flex flex-col md:flex-row rounded-2xl border border-surface-50 divide-y md:divide-x divide-surface-50">
<div className="flex flex-col w-full px-4 py-3 space-y-2">
<BodyText size={2}>Starting from</BodyText>
<DatePicker
dateTime={startDateTime}
setDateTime={setStartDateTime}
timeCaption="Start time"
className="w-full"
/>
</div>
<div className="flex flex-col w-full px-4 py-3 space-y-2">
<BodyText size={2}>Until</BodyText>
<DatePicker
dateTime={endDateTime}
setDateTime={setEndDateTime}
timeCaption="End time"
className="w-full"
fromDate={startDateTime}
/>
</div>
</div>
</div>
</div>
<p className="py-12 mx-auto w-fit text-em-low">The stackbox™</p>
<Button width="full" onClick={openConfirmStack}>
Stack Now
</Button>
Expand Down
1 change: 1 addition & 0 deletions packages/app/components/token-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./TokenPicker";
4 changes: 4 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
"@cowprotocol/cow-sdk": "^2.1.0",
"@headlessui/react": "^1.7.14",
"@headlessui/tailwindcss": "^0.1.3",
"@popperjs/core": "^2.11.8",
"class-variance-authority": "^0.6.0",
"connectkit": "^1.4.0",
"date-fns": "^2.30.0",
"next": "13.4.10",
"react": "^18.2.0",
"react-day-picker": "^8.8.0",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0",
"tailwind-merge": "^1.12.0",
"viem": "^1.0.2",
"wagmi": "^1.1.1"
Expand Down
8 changes: 8 additions & 0 deletions packages/app/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@

@tailwind components;
@tailwind utilities;

@layer base {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
Loading

0 comments on commit 016be7c

Please sign in to comment.