Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"wrangler:dev": "wrangler pages dev"
},
"dependencies": {
"@bahar/db-operations": "workspace:*",
"@bahar/fsrs": "workspace:*",
"@bahar/result": "workspace:*",
"@bahar/search": "workspace:*",
"@elysiajs/eden": "^1.4.6",
"@hookform/resolvers": "^5.2.2",
"@lingui/core": "^5.3.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/errors/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const ErrorMessage: FC<{ error: Error }> = ({ error }) => {
detail={timestamp}
/>

{isDisplayError && (
{isDisplayError && error.cause && (
<>
<ErrorDetailField
fieldName={<Trans>Cause:</Trans>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { Brain, Sparkles, PartyPopper, Archive } from "lucide-react";
import { GradeOption } from "./GradeOption";
import { GradeFeedback } from "./GradeFeedback";
import { TagBadgesList } from "./TagBadgesList";
import { formatScheduleOptions } from "./utils";
import { useDir } from "@/hooks/useDir";

interface FlashcardDrawerProps extends PropsWithChildren {
filters?: SelectDeck["filters"];
Expand Down Expand Up @@ -141,6 +143,24 @@ export const FlashcardDrawer: FC<FlashcardDrawerProps> = ({
const schedulingCards = schedulingData?.schedulingCards;
const now = schedulingData?.now ?? new Date();

const dir = useDir();
const locale = dir === "rtl" ? "ar-u-nu-arab" : "en";

const intervalLabels = useMemo(() => {
if (!schedulingCards) return null;

return formatScheduleOptions({
dates: {
[Rating.Again]: schedulingCards[Rating.Again].card.due,
[Rating.Hard]: schedulingCards[Rating.Hard].card.due,
[Rating.Good]: schedulingCards[Rating.Good].card.due,
[Rating.Easy]: schedulingCards[Rating.Easy].card.due,
},
now,
locale,
});
}, [schedulingCards, now, locale]);

const executeGrade = useCallback(
async (grade: Grade) => {
if (!schedulingCards || !currentCard) return;
Expand Down Expand Up @@ -413,7 +433,7 @@ export const FlashcardDrawer: FC<FlashcardDrawerProps> = ({

<DrawerFooter>
<AnimatePresence mode="wait">
{schedulingCards && showAnswer ? (
{schedulingCards && intervalLabels && showAnswer ? (
<motion.div
key="grading-buttons"
initial={{ opacity: 0, y: 20 }}
Expand All @@ -428,8 +448,7 @@ export const FlashcardDrawer: FC<FlashcardDrawerProps> = ({
<GradeOption
key={grade}
grade={grade}
due={schedulingCards[grade].card.due}
now={now}
intervalLabel={intervalLabels[grade]}
onClick={() => gradeCard(grade)}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { Brain, RotateCcw, ThumbsUp, Zap } from "lucide-react";
import { motion } from "motion/react";
import { FC, ReactNode, useMemo } from "react";
import { Rating } from "ts-fsrs";
import { formatInterval } from "./utils";
import { useDir } from "@/hooks/useDir";

type ReviewRating = Rating.Again | Rating.Hard | Rating.Good | Rating.Easy;

Expand All @@ -19,16 +17,14 @@ type GradeOptionConfig = {
type GradeOptionProps = {
grade: ReviewRating;
disabled?: boolean;
now: Date;
due: Date;
intervalLabel: string;
onClick: () => void;
};

export const GradeOption: FC<GradeOptionProps> = ({
grade,
onClick,
now,
due,
intervalLabel,
disabled = false,
}) => {
const options = useMemo(() => {
Expand Down Expand Up @@ -64,9 +60,6 @@ export const GradeOption: FC<GradeOptionProps> = ({
return config;
}, []);

const dir = useDir();
const locale = dir === "rtl" ? "ar-u-nu-arab" : "en";

const { label, icon, borderStyles } = options[grade];

return (
Expand All @@ -87,9 +80,7 @@ export const GradeOption: FC<GradeOptionProps> = ({
>
{icon}
<span className="font-medium">{label}</span>
<span className="text-xs text-muted-foreground">
{formatInterval(due, now, locale)}
</span>
<span className="text-xs text-muted-foreground">{intervalLabel}</span>
</Button>
</motion.div>
);
Expand Down
110 changes: 98 additions & 12 deletions apps/web/src/components/features/flashcards/FlashcardDrawer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,104 @@
import { intlFormatDistance } from "date-fns";
import { intlFormatDistance } from "@/lib/date";
import { IntlFormatDistanceUnit } from "date-fns";
import { Rating } from "ts-fsrs";

/**
* Formats the interval between two dates into a relative time string.
*/
export const formatInterval = (due: Date, now: Date, locale: string) => {
const DAYS_IN_MS = 1000 * 60 * 60 * 24;
type ReviewRating = Rating.Again | Rating.Hard | Rating.Good | Rating.Easy;

const diffMs = due.getTime() - now.getTime();
const diffDays = Math.round(diffMs / DAYS_IN_MS);
const dueOnSameDay = diffDays < 1;
export const formatInterval = ({
due,
now,
locale,
unit,
}: {
due: Date;
now: Date;
locale: string;
unit?: IntlFormatDistanceUnit;
}) => {
return intlFormatDistance(due, now, { style: "narrow", locale, unit });
};

const getSmallerUnit = (
unit: IntlFormatDistanceUnit,
): IntlFormatDistanceUnit => {
switch (unit) {
case "year":
return "month";
case "month":
return "week";
case "week":
return "day";
case "day":
return "hour";
case "hour":
return "minute";
case "minute":
return "second";
default:
return "second";
}
};

type SchedulingDates = Record<ReviewRating, Date>;

export const formatScheduleOptions = ({
dates,
now,
locale,
}: {
dates: SchedulingDates;
now: Date;
locale: string;
}): Record<ReviewRating, string> => {
const grades: ReviewRating[] = [
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
];

const results: Record<
ReviewRating,
{ label: string; unit: IntlFormatDistanceUnit }
> = {} as Record<
ReviewRating,
{ label: string; unit: IntlFormatDistanceUnit }
>;

for (const grade of grades) {
results[grade] = formatInterval({ due: dates[grade], now, locale });
}

for (let i = 0; i < grades.length - 1; i++) {
const current = grades[i];
const next = grades[i + 1];

if (
results[current].label === results[next].label &&
results[current].unit !== "second"
) {
const smallerUnit = getSmallerUnit(results[current].unit);

results[current] = formatInterval({
due: dates[current],
now,
locale,
unit: smallerUnit,
});

if (dueOnSameDay) {
return intlFormatDistance(due, now, { style: "narrow", locale });
results[next] = formatInterval({
due: dates[next],
now,
locale,
unit: smallerUnit,
});
}
}
Comment thread
Shunseii marked this conversation as resolved.

return intlFormatDistance(due, now, { style: "narrow", locale, unit: "day" });
return {
[Rating.Again]: results[Rating.Again].label,
[Rating.Hard]: results[Rating.Hard].label,
[Rating.Good]: results[Rating.Good].label,
[Rating.Easy]: results[Rating.Easy].label,
} as Record<ReviewRating, string>;
};
2 changes: 1 addition & 1 deletion apps/web/src/components/search/Highlight.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { highlightWithDiacritics } from "@/lib/search";
import { highlightWithDiacritics } from "@bahar/search";
import { useAtomValue } from "jotai";
import { FC, useMemo, memo } from "react";
import { searchQueryAtom } from "./state";
Expand Down
Loading