Repository: IraSoro/peri (ts)

Issue:
Edit Details page ui
Change display of averages (remove circle).
Add state when storage is empty.

Pull request:
Edit details page ui
Closed #100 

I changed the ui. Now it looks like this
![image](https://github.com/IraSoro/peri/assets/52165881/30cc0d19-0139-4f44-9cfa-6ef2ddf4af47)

Wide screen version:
![image](https://github.com/IraSoro/peri/assets/52165881/507d6e4f-2867-4f43-a08c-55bd90f80b91)

I also added a state when the loop array is empty. Here the user can mark periods.  Its interface looks like this
![image](https://github.com/IraSoro/peri/assets/52165881/e71c61ab-9536-462f-8f4e-0eb7adb92913)



Patch:
diff --git a/src/App.css b/src/App.css
index e5cb46b..7e21400 100644
--- a/src/App.css
+++ b/src/App.css
@@ -52,3 +52,34 @@ ion-toolbar {
 
   --background: var(--ion-color-transparent-basic);
 }
+
+#context-size {
+  max-width: 550px;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+#wide-screen {
+  width: 100%;
+  max-width: 600px;
+
+  margin-left: auto;
+  margin-right: auto;
+
+  height: 100%;
+  box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
+}
+
+ion-button {
+  --box-shadow: none;
+}
+
+ion-button.mark-button {
+  --border-radius: 16px;
+  --box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
+  width: 150px;
+}
+
+ion-list {
+  --ion-item-background: transparent;
+}
diff --git a/src/pages/TabDetails.css b/src/pages/TabDetails.css
index fbcdad7..3da2af2 100644
--- a/src/pages/TabDetails.css
+++ b/src/pages/TabDetails.css
@@ -1,34 +1,21 @@
-#rectangle-top {
-  width: 100%;
-  height: 30%;
-  background: var(--ion-color-basic);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-#rectangle-bottom {
-  width: 100%;
-  height: 70%;
-  background: #ffffff;
-  /* border-radius: 20px; */
-}
+#progress-block {
+  width: 300px;
+  height: 70px;
+  background: var(--ion-color-light);
+  border-radius: 20px;
 
-#circle {
-  width: 180px;
-  height: 180px;
-  background: #ffffff;
-  border-color: #d7d7d7;
-  border-style: solid;
-  border-width: 10px 10px 10px 10px;
-  border-radius: 50%;
   display: flex;
-  align-items: center;
+  flex-direction: column;
   justify-content: center;
 }
 
 ion-progress-bar {
-  height: 60%;
+  height: 8px;
+  border-radius: 20px;
+}
+
+ion-progress-bar.current-progress {
+  height: 8px;
   border-radius: 20px;
 }
 
@@ -39,12 +26,12 @@ ion-progress-bar::part(progress) {
 
 ion-progress-bar::part(track) {
   border-radius: 20px;
-  background: var(--ion-color-basic);
+  background: var(--ion-color-light-basic);
 }
 
-.current-progress::part(track) {
+ion-progress-bar.current-progress::part(track) {
   border-radius: 20px;
-  background: #d7d7d7;
+  background: var(--ion-color-transparent-basic);
 }
 
 ion-progress-bar::part(stream) {
@@ -52,15 +39,26 @@ ion-progress-bar::part(stream) {
   background: #ffffff;
 }
 
-ion-item {
-  --min-height: 60px;
+#average-length {
+  width: 300px;
+  height: 75px;
+  background: var(--ion-color-dark-basic);
+  border-radius: 20px;
+
+  display: flex;
 }
 
-.transparent-center {
-  --ion-item-background: transparent;
+#inline-block {
+  width: 149px;
+  background: var(--ion-color-dark-basic);
+  border-radius: 20px;
 
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  height: 100%;
+  margin-left: 15px;
+  margin-right: 15px;
+}
+
+#vertical-line {
+  width: 2px;
+  height: 55px;
+  background: var(--ion-color-light);
 }
diff --git a/src/pages/TabDetails.tsx b/src/pages/TabDetails.tsx
index f3a2b76..bba47cd 100644
--- a/src/pages/TabDetails.tsx
+++ b/src/pages/TabDetails.tsx
@@ -4,9 +4,10 @@ import {
   IonPage,
   IonLabel,
   IonProgressBar,
-  IonItem,
   IonList,
+  IonCol,
 } from "@ionic/react";
+import { useTranslation } from "react-i18next";
 import "./TabDetails.css";
 
 import {
@@ -14,36 +15,10 @@ import {
   getAverageLengthOfPeriod,
   getDayOfCycle,
   getLastStartDate,
+  getFormattedDate,
 } from "../state/CalculationLogics";
-import { useTranslation } from "react-i18next";
 import { CyclesContext } from "../state/Context";
 
-function useTitleLastCycle() {
-  const cycles = useContext(CyclesContext).cycles;
-  const dayOfCycle = getDayOfCycle(cycles);
-  const { t } = useTranslation();
-
-  if (!dayOfCycle) {
-    return "";
-  }
-
-  return `${dayOfCycle} ${t("Days_interval", {
-    postProcess: "interval",
-    count: 1, // NOTE: to indicate which day is in the account, you need to write the day as if in the singular
-  })}`;
-}
-
-function useProgressBarBuffer() {
-  const cycles = useContext(CyclesContext).cycles;
-  const dayOfCycle = getDayOfCycle(cycles);
-  const defaultLengthOfCycle = 28;
-
-  if (!dayOfCycle) {
-    return defaultLengthOfCycle;
-  }
-  return Number(dayOfCycle);
-}
-
 interface InfoOneCycle {
   lengthOfCycleString: string;
   lengthOfCycleNumber: number;
@@ -51,7 +26,7 @@ interface InfoOneCycle {
   dates: string;
 }
 
-export function useInfoForOneCycle(idx: number): InfoOneCycle {
+function useInfoForOneCycle(idx: number): InfoOneCycle {
   const cycles = useContext(CyclesContext).cycles;
   const { t } = useTranslation();
 
@@ -62,7 +37,7 @@ export function useInfoForOneCycle(idx: number): InfoOneCycle {
       lengthOfCycleNumber: defaultLengthOfCycle,
       lengthOfCycleString: t("Cycle length"),
       lengthOfPeriod: 0,
-      dates: t("date"),
+      dates: "",
     };
   }
   const cycleLenNumber = cycles[idx].cycleLength;
@@ -76,7 +51,10 @@ export function useInfoForOneCycle(idx: number): InfoOneCycle {
   const dateStart: Date = new Date(cycles[idx].startDate);
   const dateFinish: Date = new Date(cycles[idx].startDate);
   dateFinish.setDate(dateFinish.getDate() + cycleLenNumber - 1);
-  const dates = `${dateStart.toLocaleDateString()} - ${dateFinish.toLocaleDateString()}`;
+  const dates = `${getFormattedDate(
+    dateStart,
+    t("locale"),
+  )} - ${getFormattedDate(dateFinish, t("locale"))}`;
 
   return {
     lengthOfCycleNumber: cycleLenNumber,
@@ -86,45 +64,62 @@ export function useInfoForOneCycle(idx: number): InfoOneCycle {
   };
 }
 
-const CurrentCycle = () => {
-  const title = useTitleLastCycle();
-  const progressBarBuffer = useProgressBarBuffer();
+const lenCycleStyle = {
+  fontSize: "13px" as const,
+  color: "var(--ion-color-black)" as const,
+  textAlign: "left" as const,
+};
 
-  const cycles = useContext(CyclesContext).cycles;
-  const startDate = getLastStartDate(cycles);
-  const lengthOfPeriod = getAverageLengthOfPeriod(cycles);
+const datesStyle = {
+  fontSize: "11px" as const,
+  color: "var(--ion-color-medium)" as const,
+  textAlign: "left" as const,
+};
+
+const progressBarStyle = {
+  marginTop: "5px" as const,
+  marginBottom: "5px" as const,
+};
 
+function setBufferProgress(value: number) {
+  return (value / 100) * 3;
+}
+
+function setValueProgress(lengthOfPeriod: number, dayOfCycle: number) {
+  return dayOfCycle < lengthOfPeriod
+    ? (dayOfCycle / 100) * 3
+    : (lengthOfPeriod / 100) * 3;
+}
+
+const CurrentCycle = () => {
+  const cycles = useContext(CyclesContext).cycles;
   const { t } = useTranslation();
+  const dayOfCycle = getDayOfCycle(cycles);
+  const title = `${dayOfCycle} ${t("Days_interval", {
+    postProcess: "interval",
+    count: 1, // NOTE: to indicate which day is in the account, you need to write the day as if in the singular
+  })}`;
+
+  const startDate = new Date(getLastStartDate(cycles));
+  const lengthOfPeriod = getAverageLengthOfPeriod(cycles);
 
   return (
-    <IonItem
-      class="transparent-center"
-      lines="none"
-    >
-      <IonLabel position="stacked">
-        {title ? (
-          <h2>{`${t("Current cycle")}: ${title}`}</h2>
-        ) : (
-          <h2>{t("Current cycle")}</h2>
-        )}
-      </IonLabel>
-      <IonLabel position="stacked">
-        {startDate ? (
-          <p>{`${t("Started date")} ${new Date(
-            startDate,
-          ).toLocaleDateString()}`}</p>
-        ) : (
-          <p>{t("Started date")}</p>
-        )}
-      </IonLabel>
-      <IonLabel position="stacked">
+    <div id="progress-block">
+      <div style={{ marginLeft: "15px" }}>
+        <IonLabel>
+          <p style={lenCycleStyle}>{title}</p>
+        </IonLabel>
         <IonProgressBar
           class="current-progress"
-          value={(lengthOfPeriod / 100) * 3}
-          buffer={(progressBarBuffer / 100) * 3}
-        ></IonProgressBar>
-      </IonLabel>
-    </IonItem>
+          style={progressBarStyle}
+          value={setValueProgress(lengthOfPeriod, dayOfCycle)}
+          buffer={setBufferProgress(dayOfCycle)}
+        />
+        <IonLabel>
+          <p style={datesStyle}>{getFormattedDate(startDate, t("locale"))}</p>
+        </IonLabel>
+      </div>
+    </div>
   );
 };
 
@@ -132,38 +127,47 @@ interface IdxProps {
   idx: number;
 }
 
-const ItemProgress = (props: IdxProps) => {
-  const info = useInfoForOneCycle(props.idx);
+const ListProgress = () => {
+  const cycles = useContext(CyclesContext).cycles;
+
+  const ItemProgress = (props: IdxProps) => {
+    const info = useInfoForOneCycle(props.idx + 1);
+    const dayOfCycle = getDayOfCycle(cycles);
 
-  return (
-    <IonItem
-      class="transparent-center"
-      lines="none"
-    >
-      <IonLabel position="stacked">
-        <h2>{info.lengthOfCycleString}</h2>
-      </IonLabel>
-      <IonLabel position="stacked">
-        <p>{info.dates}</p>
-      </IonLabel>
-      <IonLabel position="stacked">
-        <IonProgressBar
-          value={(info.lengthOfPeriod / 100) * 3}
-          buffer={(info.lengthOfCycleNumber / 100) * 3}
-        ></IonProgressBar>
-      </IonLabel>
-    </IonItem>
-  );
-};
+    return (
+      <div
+        id="progress-block"
+        style={{ marginTop: "15px" }}
+      >
+        <div style={{ marginLeft: "15px" }}>
+          <IonLabel>
+            <p style={lenCycleStyle}>{info.lengthOfCycleString}</p>
+          </IonLabel>
+          <IonProgressBar
+            style={progressBarStyle}
+            value={setValueProgress(info.lengthOfPeriod, dayOfCycle)}
+            buffer={setBufferProgress(info.lengthOfCycleNumber)}
+          />
+          <IonLabel>
+            <p style={datesStyle}>{info.dates}</p>
+          </IonLabel>
+        </div>
+      </div>
+    );
+  };
 
-const ListProgress = () => {
-  const numbers = [1, 2, 3, 4, 5];
-  const list = numbers.map((idx) => (
-    <ItemProgress
-      key={idx}
-      idx={idx}
-    />
-  ));
+  const list = cycles
+    .filter((_value, idx) => {
+      return idx > 0;
+    })
+    .map((_item, idx) => {
+      return (
+        <ItemProgress
+          key={idx}
+          idx={idx}
+        />
+      );
+    });
 
   return <>{list}</>;
 };
@@ -186,40 +190,69 @@ const TabDetails = () => {
   })}`;
 
   const p_style = {
-    fontSize: "10px" as const,
-    color: "var(--ion-color-basic)" as const,
-    textAlign: "center" as const,
+    fontSize: "13px" as const,
+    color: "var(--ion-color-light)" as const,
+    textAlign: "left" as const,
+    marginBottom: "5px" as const,
   };
 
   const h_style = {
-    fontWeight: "bold" as const,
-    color: "var(--ion-color-dark-basic)" as const,
-    textAlign: "center" as const,
+    fontSize: "20px" as const,
+    color: "var(--ion-color-light)" as const,
+    textAlign: "left" as const,
   };
 
   return (
-    <IonPage>
-      <IonContent
-        color="basic"
-        fullscreen
-      >
-        <div id="rectangle-top">
-          <div id="circle">
-            <IonLabel>
-              <p style={p_style}>{t("Period length")}</p>
-              <h1 style={h_style}>{lengthOfPeriod}</h1>
-              <p style={p_style}>{t("Cycle length")}</p>
-              <h1 style={h_style}>{lengthOfCycle}</h1>
-            </IonLabel>
+    <IonPage style={{ backgroundColor: "var(--ion-color-background)" }}>
+      <div id="wide-screen">
+        <IonContent
+          className="ion-padding"
+          color="transparent-basic"
+        >
+          <div id="context-size">
+            <IonCol>
+              <div
+                id="average-length"
+                style={{ marginBottom: "15px" }}
+              >
+                <IonCol>
+                  <div id="inline-block">
+                    <IonLabel style={{ marginBottom: "10px" }}>
+                      <p style={p_style}>{t("Cycle length")}</p>
+                      <p style={h_style}>
+                        {averageLengthOfCycle ? lengthOfCycle : "---"}
+                      </p>
+                    </IonLabel>
+                  </div>
+                  <div id="vertical-line" />
+                  <div id="inline-block">
+                    <IonLabel>
+                      <p style={p_style}>{t("Period length")}</p>
+                      <p style={h_style}>
+                        {averageLengthOfPeriod ? lengthOfPeriod : "---"}
+                      </p>
+                    </IonLabel>
+                  </div>
+                </IonCol>
+              </div>
+            </IonCol>
+            <IonCol>
+              {cycles.length > 0 ? (
+                <IonList>
+                  <CurrentCycle />
+                  {cycles.length > 1 && <ListProgress />}
+                </IonList>
+              ) : (
+                <div id="progress-block">
+                  <p style={{ fontSize: "13px" }}>
+                    {t("You haven't marked any periods yet")}
+                  </p>
+                </div>
+              )}
+            </IonCol>
           </div>
-        </div>
-        <div id="rectangle-bottom">
-          <IonList class="transparent-center">
-            <CurrentCycle />
-            <ListProgress />
-          </IonList>
-        </div>
-      </IonContent>
+        </IonContent>
+      </div>
     </IonPage>
   );
 };
diff --git a/src/pages/TabHome.css b/src/pages/TabHome.css
index a53b6eb..e69de29 100644
--- a/src/pages/TabHome.css
+++ b/src/pages/TabHome.css
@@ -1,32 +0,0 @@
-ion-button {
-  --box-shadow: none;
-}
-
-.mark-button {
-  --border-radius: 16px;
-  --box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
-  width: 150px;
-}
-
-#block {
-  max-width: 550px;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.transparent {
-  --ion-item-background: transparent;
-}
-
-#wide-screen {
-  width: 100%;
-  max-width: 600px;
-
-  margin-left: auto;
-  margin-right: auto;
-
-  height: 100%;
-  /* max-height: 700px; */
-
-  box-shadow: 0 2px 6px 0 rgb(0, 0, 0, 0.25);
-}
diff --git a/src/pages/TabHome.tsx b/src/pages/TabHome.tsx
index 37f5f70..96048df 100644
--- a/src/pages/TabHome.tsx
+++ b/src/pages/TabHome.tsx
@@ -127,7 +127,6 @@ const TabHome = (props: HomeProps) => {
         <IonContent
           className="ion-padding"
           color="transparent-basic"
-          class="home"
         >
           <Welcome
             isOpen={isWelcomeModal}
@@ -135,7 +134,7 @@ const TabHome = (props: HomeProps) => {
             isLanguageModal={props.isLanguageModal}
             setIsLanguageModal={props.setIsLanguageModal}
           />
-          <div id="block">
+          <div id="context-size">
             <div style={{ marginTop: "30px", marginBottom: "30px" }}>
               <IonLabel>
                 <p style={{ fontSize: "40px", color: "var(--ion-color-dark)" }}>
diff --git a/src/state/CalculationLogics.ts b/src/state/CalculationLogics.ts
index d2ca77a..f98ff1c 100644
--- a/src/state/CalculationLogics.ts
+++ b/src/state/CalculationLogics.ts
@@ -1,6 +1,7 @@
 import i18n from "i18next";
 import { Cycle } from "../data/ClassCycle";
 import { format } from "date-fns";
+import { ru } from "date-fns/locale";
 
 const millisecondsInDay = 24 * 60 * 60 * 1000;
 
@@ -26,7 +27,7 @@ export function getOvulationStatus(cycles: Cycle[]) {
   }
 
   const cycleLength = getAverageLengthOfCycle(cycles);
-  const dayOfCycle = Number(getDayOfCycle(cycles));
+  const dayOfCycle = getDayOfCycle(cycles);
 
   const lutealPhaseLength = 14;
   const ovulationDay = cycleLength - lutealPhaseLength;
@@ -54,7 +55,7 @@ export function getPregnancyChance(cycles: Cycle[]) {
     return "";
   }
 
-  const dayOfCycle = Number(getDayOfCycle(cycles));
+  const dayOfCycle = getDayOfCycle(cycles);
   const cycleLength = getAverageLengthOfCycle(cycles);
 
   const lutealPhaseLength = 14;
@@ -113,7 +114,7 @@ export function getDaysBeforePeriod(cycles: Cycle[]) {
 
 export function getDayOfCycle(cycles: Cycle[]) {
   if (cycles.length === 0) {
-    return "";
+    return 0;
   }
 
   const start = new Date(cycles[0].startDate);
@@ -125,7 +126,7 @@ export function getDayOfCycle(cycles: Cycle[]) {
     Math.ceil((currentDate.getTime() - start.getTime()) / millisecondsInDay) +
     1;
 
-  return diff.toString();
+  return diff;
 }
 
 export function getPhase(
@@ -409,3 +410,16 @@ export function isForecastPeriodDays(date: Date, cycles: Cycle[]) {
 
   return false;
 }
+
+export function getFormattedDate(date: Date, language: string) {
+  const formattedDates = new Map<string, string>([
+    ["en-GB", format(date, "MMM d")],
+    [
+      "ru",
+      format(date, "d MMMM", {
+        locale: ru,
+      }),
+    ],
+  ]);
+  return formattedDates.get(language) ?? format(date, "MMM d");
+}
diff --git a/src/translations/ru.tsx b/src/translations/ru.tsx
index 042ec14..d0f6ad5 100644
--- a/src/translations/ru.tsx
+++ b/src/translations/ru.tsx
@@ -22,10 +22,7 @@ const ru = {
   Details: "Детали",
   "Period length": "Длина месячных",
   "Cycle length": "Длина цикла",
-  "Current cycle": "Текущий цикл",
-  started: "Начался",
-  "Started date": "Дата начала",
-  date: "дата",
+  "You haven't marked any periods yet": "Вы еще не отметили дни месячных",
   // Mark Modal
   mark: "отметить",
   "Select date range": "Отметьте диапазон месячных",


Test patch:
diff --git a/src/tests/CalculationLogic.test.ts b/src/tests/CalculationLogic.test.ts
index 9f44434..30b63b2 100644
--- a/src/tests/CalculationLogic.test.ts
+++ b/src/tests/CalculationLogic.test.ts
@@ -1,6 +1,7 @@
 import i18n from "i18next";
 import { Cycle } from "../data/ClassCycle";
 import { format } from "date-fns";
+import { ru } from "date-fns/locale";
 import {
   getOvulationStatus,
   getPregnancyChance,
@@ -16,6 +17,7 @@ import {
   getPastFuturePeriodDays,
   getLastStartDate,
   getLengthOfLastPeriod,
+  getFormattedDate,
 } from "../state/CalculationLogics";
 
 describe("getOvulationStatus", () => {
@@ -178,7 +180,7 @@ describe("getPregnancyChance", () => {
 
 describe("getDayOfCycle", () => {
   test("cycles array is empty", () => {
-    expect(getDayOfCycle([])).toEqual("");
+    expect(getDayOfCycle([])).toEqual(0);
   });
 
   test("middle of the cycle", () => {
@@ -200,7 +202,7 @@ describe("getDayOfCycle", () => {
       });
     }
 
-    expect(getDayOfCycle(cycles)).toEqual("14");
+    expect(getDayOfCycle(cycles)).toEqual(14);
   });
 });
 
@@ -965,3 +967,27 @@ describe("getLengthOfLastPeriod", () => {
     expect(getLengthOfLastPeriod(cycles)).toEqual(cycles[0].periodLength);
   });
 });
+
+describe("getFormattedDate", () => {
+  test("English date format", () => {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    expect(getFormattedDate(today, "en-GB")).toEqual(format(today, "MMM d"));
+  });
+
+  test("Russian date format", () => {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    expect(getFormattedDate(today, "ru")).toEqual(
+      format(today, "d MMMM", {
+        locale: ru,
+      }),
+    );
+  });
+
+  test("Unknown date format", () => {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    expect(getFormattedDate(today, "1")).toEqual(format(today, "MMM d"));
+  });
+});
diff --git a/src/tests/TabHome.test.tsx b/src/tests/TabHome.test.tsx
index ef02da2..ec7afb9 100644
--- a/src/tests/TabHome.test.tsx
+++ b/src/tests/TabHome.test.tsx
@@ -1,7 +1,14 @@
 import { render } from "@testing-library/react";
-import TabDetails from "../pages/TabDetails";
+import TabHome from "../pages/TabHome";
 
 test("renders without crashing", () => {
-  const { baseElement } = render(<TabDetails />);
+  const { baseElement } = render(
+    <TabHome
+      isLanguageModal
+      setIsLanguageModal={() => {}}
+      isEditModal
+      setIsEditModal={() => {}}
+    />,
+  );
   expect(baseElement).toBeDefined();
 });