-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* User statistics * db types
- Loading branch information
Showing
11 changed files
with
381 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { css } from "@emotion/css"; | ||
import React from "react"; | ||
|
||
export const LegendItem = (props: { color: string }) => { | ||
const { color } = props; | ||
return ( | ||
<div | ||
className={css({ | ||
height: 14, | ||
width: 14, | ||
backgroundColor: color, | ||
borderRadius: 4 | ||
})} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { colord } from "colord"; | ||
import { theme } from "../../ui/theme.tsx"; | ||
import React, { useEffect, useRef } from "react"; | ||
|
||
export type PieChartData = { | ||
interval_range: string; | ||
frequency: number; | ||
}; | ||
|
||
export const chartStart = colord(theme.buttonColorLighter) | ||
.lighten(0.05) | ||
.toRgbString(); | ||
|
||
export const chartFinish = colord(theme.buttonColorComputed) | ||
.darken(0.2) | ||
.toRgbString(); | ||
|
||
const interpolateColor = ( | ||
color1: string, | ||
color2: string, | ||
factor: number, | ||
): string => { | ||
// Assumes color1 and color2 are CSS color strings "rgb(r, g, b)" | ||
const result = color1 | ||
.slice(4, -1) | ||
.split(",") | ||
.map(Number) | ||
.map((c1, i) => { | ||
const c2 = Number(color2.slice(4, -1).split(",")[i]); | ||
return Math.round(c1 + (c2 - c1) * factor); | ||
}); | ||
return `rgb(${result.join(", ")})`; | ||
}; | ||
|
||
type Props = { | ||
data: PieChartData[]; | ||
width: number; | ||
height: number; | ||
}; | ||
|
||
export const PieChartCanvas = ({ data, width, height }: Props) => { | ||
const canvasRef = useRef<HTMLCanvasElement>(null); | ||
|
||
useEffect(() => { | ||
if (!canvasRef.current) { | ||
return; | ||
} | ||
const canvas = canvasRef.current; | ||
const ctx = canvas.getContext("2d"); | ||
if (!ctx) { | ||
return; | ||
} | ||
|
||
ctx.clearRect(0, 0, width, height); | ||
|
||
const totalFrequency = data.reduce((acc, item) => acc + item.frequency, 0); | ||
let startAngle = 0; | ||
|
||
data.forEach((item, index) => { | ||
const sliceAngle = (item.frequency / totalFrequency) * 2 * Math.PI; | ||
const endAngle = startAngle + sliceAngle; | ||
|
||
ctx.beginPath(); | ||
ctx.moveTo(width / 2, height / 2); | ||
ctx.arc( | ||
width / 2, | ||
height / 2, | ||
Math.min(width, height) / 2, | ||
startAngle, | ||
endAngle, | ||
); | ||
ctx.closePath(); | ||
|
||
ctx.fillStyle = interpolateColor( | ||
chartStart, | ||
chartFinish, | ||
index / (data.length - 1), | ||
); | ||
ctx.fill(); | ||
|
||
startAngle = endAngle; | ||
}); | ||
}, [data, height, width]); | ||
|
||
return <canvas ref={canvasRef} width={width} height={height} />; | ||
}; |
19 changes: 19 additions & 0 deletions
19
src/screens/user-statistics/store/user-statistics-store-context.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { createContext, ReactNode, useContext } from "react"; | ||
import { UserStatisticsStore } from "./user-statistics-store.ts"; | ||
import { assert } from "../../../lib/typescript/assert.ts"; | ||
|
||
const Context = createContext<UserStatisticsStore | null>(null); | ||
|
||
export const UserStatisticsStoreProvider = (props: { children: ReactNode }) => { | ||
return ( | ||
<Context.Provider value={new UserStatisticsStore()}> | ||
{props.children} | ||
</Context.Provider> | ||
); | ||
}; | ||
|
||
export const useUserStatisticsStore = () => { | ||
const store = useContext(Context); | ||
assert(store, "UserStatisticsStoreProvider not found"); | ||
return store; | ||
}; |
47 changes: 47 additions & 0 deletions
47
src/screens/user-statistics/store/user-statistics-store.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { makeAutoObservable } from "mobx"; | ||
import { fromPromise, IPromiseBasedObservable } from "mobx-utils"; | ||
import { MyStatisticsResponse } from "../../../../functions/my-statistics.ts"; | ||
import { myStatisticsRequest } from "../../../api/api.ts"; | ||
import { PieChartData } from "../pie-chart-canvas.tsx"; | ||
|
||
export class UserStatisticsStore { | ||
userStatistics?: IPromiseBasedObservable<MyStatisticsResponse>; | ||
|
||
constructor() { | ||
makeAutoObservable(this, {}, { autoBind: true }); | ||
} | ||
|
||
load() { | ||
this.userStatistics = fromPromise(myStatisticsRequest()); | ||
} | ||
|
||
get isLoading() { | ||
return this.userStatistics?.state === "pending"; | ||
} | ||
|
||
get know() { | ||
if (this.userStatistics?.state !== "fulfilled") { | ||
return 0; | ||
} | ||
return this.userStatistics.value.cardsLearning.know ?? 0; | ||
} | ||
|
||
get learning() { | ||
if (this.userStatistics?.state !== "fulfilled") { | ||
return 0; | ||
} | ||
return this.userStatistics.value.cardsLearning.learning ?? 0; | ||
} | ||
|
||
get total() { | ||
return this.know + this.learning; | ||
} | ||
|
||
get frequencyChart(): PieChartData[] { | ||
if (this.userStatistics?.state !== "fulfilled") { | ||
return []; | ||
} | ||
|
||
return this.userStatistics.value.intervalFrequency; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { Screen } from "../shared/screen.tsx"; | ||
import { observer } from "mobx-react-lite"; | ||
import { useMount } from "../../lib/react/use-mount.ts"; | ||
import { useUserStatisticsStore } from "./store/user-statistics-store-context.tsx"; | ||
import { useBackButton } from "../../lib/telegram/use-back-button.tsx"; | ||
import { screenStore } from "../../store/screen-store.ts"; | ||
import { CardRow } from "../../ui/card-row.tsx"; | ||
import { HintTransparent } from "../../ui/hint-transparent.tsx"; | ||
import React from "react"; | ||
import { css } from "@emotion/css"; | ||
import { t } from "../../translations/t.ts"; | ||
import { | ||
chartFinish, | ||
chartStart, | ||
PieChartCanvas, | ||
} from "./pie-chart-canvas.tsx"; | ||
import { LegendItem } from "./legend-item.tsx"; | ||
import { DeckLoading } from "../shared/deck-loading.tsx"; | ||
|
||
export const UserStatisticsScreen = observer(() => { | ||
const userStatisticsStore = useUserStatisticsStore(); | ||
|
||
useBackButton(() => { | ||
screenStore.back(); | ||
}); | ||
|
||
useMount(() => { | ||
userStatisticsStore.load(); | ||
}); | ||
|
||
return ( | ||
<Screen title={t("user_stats_page")}> | ||
{userStatisticsStore.isLoading ? ( | ||
<DeckLoading speed={1} /> | ||
) : ( | ||
<CardRow> | ||
<span>{t("user_stats_remembered")}</span> | ||
<span>{userStatisticsStore.know}</span> | ||
</CardRow> | ||
)} | ||
<HintTransparent>{t("user_stats_remembered_hint")}</HintTransparent> | ||
|
||
{userStatisticsStore.isLoading ? ( | ||
<DeckLoading speed={1} /> | ||
) : ( | ||
<CardRow> | ||
<span>{t("user_stats_learning")}</span> | ||
<span>{userStatisticsStore.learning}</span> | ||
</CardRow> | ||
)} | ||
<HintTransparent>{t("user_stats_learning_hint")}</HintTransparent> | ||
|
||
{userStatisticsStore.isLoading ? ( | ||
<DeckLoading speed={1} /> | ||
) : ( | ||
<CardRow> | ||
<span>{t("user_stats_total")}</span> | ||
<span>{userStatisticsStore.total}</span> | ||
</CardRow> | ||
)} | ||
<HintTransparent>{t("user_stats_total_hint")}</HintTransparent> | ||
|
||
{!userStatisticsStore.isLoading ? ( | ||
<> | ||
<div | ||
className={css({ | ||
marginTop: 10, | ||
marginLeft: "auto", | ||
marginRight: "auto", | ||
textAlign: "center", | ||
fontWeight: 500, | ||
})} | ||
> | ||
{t("user_stats_learning_time")} | ||
</div> | ||
|
||
<div | ||
className={css({ | ||
marginTop: 10, | ||
marginLeft: "auto", | ||
marginRight: "auto", | ||
})} | ||
> | ||
<PieChartCanvas | ||
data={userStatisticsStore.frequencyChart} | ||
width={250} | ||
height={200} | ||
/> | ||
</div> | ||
|
||
<div | ||
className={css({ | ||
display: "flex", | ||
flexDirection: "column", | ||
gap: 4, | ||
alignSelf: "center", | ||
})} | ||
> | ||
<div | ||
className={css({ display: "flex", gap: 4, alignItems: "center" })} | ||
> | ||
<LegendItem color={chartStart} /> | ||
<span className={css({ fontSize: 14 })}> | ||
{t("user_stats_chart_min_expl")} | ||
</span> | ||
</div> | ||
<div | ||
className={css({ display: "flex", gap: 4, alignItems: "center" })} | ||
> | ||
<LegendItem color={chartFinish} /> | ||
<span className={css({ fontSize: 14 })}> | ||
{t("user_stats_chart_max_expl")} | ||
</span> | ||
</div> | ||
</div> | ||
<p> | ||
<HintTransparent> | ||
{t("user_stats_learning_time_hint")} | ||
</HintTransparent> | ||
</p> | ||
</> | ||
) : null} | ||
</Screen> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.