Skip to content

Commit

Permalink
Card sort (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk authored Dec 8, 2023
1 parent ffceec2 commit 4471516
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 44 deletions.
20 changes: 10 additions & 10 deletions src/lib/mobx-form/form-has-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ test("is form empty", () => {
expect(isFormEmpty(f)).toBeTruthy();
});

test('very nested form - only fields', () => {
test("very nested form - only fields", () => {
const f = {
a: new TextField("a", validators.required()),
b: {
Expand All @@ -108,15 +108,15 @@ test('very nested form - only fields', () => {
};

expect(isFormValid(f)).toBeTruthy();
expect(isFormTouched(f)).toBeFalsy()
expect(isFormTouched(f)).toBeFalsy();

f.b.c.d.onChange('');
f.b.c.d.onChange("");

expect(isFormTouched(f)).toBeTruthy()
expect(isFormTouched(f)).toBeTruthy();
expect(isFormValid(f)).toBeFalsy();
})
});

test('very nested form - any fields', () => {
test("very nested form - any fields", () => {
const f = {
a: new TextField("a", validators.required()),
num: 12,
Expand All @@ -129,10 +129,10 @@ test('very nested form - any fields', () => {
};

expect(isFormValid(f)).toBeTruthy();
expect(isFormTouched(f)).toBeFalsy()
expect(isFormTouched(f)).toBeFalsy();

f.b.c.d.onChange('');
f.b.c.d.onChange("");

expect(isFormTouched(f)).toBeTruthy()
expect(isFormTouched(f)).toBeTruthy();
expect(isFormValid(f)).toBeFalsy();
})
});
11 changes: 8 additions & 3 deletions src/lib/mobx-form/form-has-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ const walkAndCheck = (
if (Array.isArray(value)) {
return value[iterateArray](check);
}
if (typeof value === 'object' && value !== null) {
return Object.values(value)[iterateArray](walkAndCheck(check, iterateArray, defaultValue));
if (typeof value === "object" && value !== null) {
return Object.values(value)[iterateArray](
walkAndCheck(check, iterateArray, defaultValue),
);
}
return defaultValue;
});
Expand All @@ -30,7 +32,10 @@ export const isFormTouchedAndHasError = walkAndCheck(

export const isFormTouched = walkAndCheck((field) => field.isTouched, "some");
export const isFormValid = walkAndCheck((field) => !field.error, "every", true);
export const isFormTouchedAndValid = walkAndCheck(field => field.isTouched && !field.error, "some");
export const isFormTouchedAndValid = walkAndCheck(
(field) => field.isTouched && !field.error,
"some",
);
export const isFormEmpty = walkAndCheck((field) => !field.value, "every");

export const formTouchAll = (form: Form) => {
Expand Down
13 changes: 10 additions & 3 deletions src/lib/rollbar/rollbar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
export const reportHandledError = (description: string, e?: any, info?: any) => {
export const reportHandledError = (
description: string,
e?: any,
info?: any,
) => {
console.error(e);
// @ts-ignore
Rollbar.error(description, e, info);
};

let reported = false;

export const reportHandledErrorOnce = (description: string, e?: any, info?: any) => {
export const reportHandledErrorOnce = (
description: string,
e?: any,
info?: any,
) => {
if (reported) {
return;
}
Expand All @@ -15,4 +23,3 @@ export const reportHandledErrorOnce = (description: string, e?: any, info?: any)
// @ts-ignore
Rollbar.error(description, e, info);
};

6 changes: 2 additions & 4 deletions src/screens/deck-form/card-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { useMainButton } from "../../lib/telegram/use-main-button.tsx";
import { useDeckFormStore } from "../../store/deck-form-store-context.tsx";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { CardFormView } from "./card-form-view.tsx";
import {
useTelegramProgress
} from "../../lib/telegram/use-telegram-progress.tsx";
import { useTelegramProgress } from "../../lib/telegram/use-telegram-progress.tsx";

export const CardForm = observer(() => {
const deckFormStore = useDeckFormStore();
Expand All @@ -22,7 +20,7 @@ export const CardForm = observer(() => {
() => deckFormStore.isSaveCardButtonActive,
);

useTelegramProgress(() => deckFormStore.isSending)
useTelegramProgress(() => deckFormStore.isSending);

useBackButton(() => {
deckFormStore.onCardBack();
Expand Down
68 changes: 64 additions & 4 deletions src/screens/deck-form/card-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useDeckFormStore } from "../../store/deck-form-store-context.tsx";
import { screenStore } from "../../store/screen-store.ts";
import { assert } from "../../lib/typescript/assert.ts";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { css } from "@emotion/css";
import { css, cx } from "@emotion/css";
import { Input } from "../../ui/input.tsx";
import { theme } from "../../ui/theme.tsx";
import { Button } from "../../ui/button.tsx";
import React from "react";
import { reset } from "../../ui/reset.ts";

export const CardList = observer(() => {
const deckFormStore = useDeckFormStore();
Expand All @@ -28,12 +29,71 @@ export const CardList = observer(() => {
display: "flex",
flexDirection: "column",
gap: 6,
marginBottom: 16
marginBottom: 16,
})}
>
<h4 className={css({ textAlign: "center" })}>Cards</h4>
{deckFormStore.form.cards.length > 1 && (
<Input field={deckFormStore.cardFilter} placeholder={"Search card"} />
<>
<Input
field={deckFormStore.cardFilter.text}
placeholder={"Search card"}
/>
<div
className={css({
display: "flex",
marginLeft: 12,
gap: 8,
})}
>
<span>Sort by</span>
{[
{
label: "Date",
fieldName: "createdAt" as const,
},
{
label: "Front",
fieldName: "frontAlpha" as const,
},
{
label: "Back",
fieldName: "backAlpha" as const,
},
].map((item, i) => {
const isSelected =
deckFormStore.cardFilter.sortBy.value === item.fieldName;
const isAsc =
deckFormStore.cardFilter.sortDirection.value === "asc";

return (
<button
key={i}
className={cx(
reset.button,
css({
color: isSelected ? theme.linkColor : undefined,
fontSize: 16,
}),
)}
onClick={() => {
if (
deckFormStore.cardFilter.sortBy.value === item.fieldName
) {
deckFormStore.cardFilter.sortDirection.onChange(
isAsc ? "desc" : "asc",
);
} else {
deckFormStore.cardFilter.sortBy.onChange(item.fieldName);
}
}}
>
{item.label} {isSelected ? (isAsc ? "↓" : "↑") : null}
</button>
);
})}
</div>
</>
)}
{deckFormStore.filteredCards.map((cardForm, i) => (
<div
Expand All @@ -45,7 +105,7 @@ export const CardList = observer(() => {
cursor: "pointer",
backgroundColor: theme.secondaryBgColor,
borderRadius: theme.borderRadius,
padding: 12
padding: 12,
})}
>
<div>{cardForm.front.value}</div>
Expand Down
38 changes: 37 additions & 1 deletion src/store/deck-form-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { DeckFormStore } from "./deck-form-store.ts";
import { CardFormType, DeckFormStore } from "./deck-form-store.ts";
import { DeckCardDbType } from "../../functions/db/deck/decks-with-cards-schema.ts";
import { type DeckWithCardsWithReviewType } from "./deck-list-store.ts";
import { assert } from "../lib/typescript/assert.ts";
Expand Down Expand Up @@ -226,4 +226,40 @@ describe("deck form store", () => {

expect(store.form.cards).toHaveLength(3);
});

it("sorting", () => {
const store = new DeckFormStore();
store.loadForm();
assert(store.form);
expect(store.form.cards).toHaveLength(3);

const cardToId = (card: CardFormType) => card.id;

expect(store.filteredCards.map(cardToId)).toEqual([5, 4, 3]);

store.cardFilter.sortDirection.onChange("asc");

expect(store.filteredCards.map(cardToId)).toEqual([3, 4, 5]);

store.cardFilter.sortBy.onChange("frontAlpha");

expect(store.filteredCards.map(cardToId)).toEqual([3, 5, 4]);

store.cardFilter.sortDirection.onChange("desc");

expect(store.filteredCards.map(cardToId)).toEqual([4, 5, 3]);

store.openNewCardForm();

expect(store.filteredCards.map(cardToId)).toEqual([4, 5, 3, undefined]);

store.cardFilter.sortBy.onChange("createdAt");
store.cardFilter.sortDirection.onChange("asc");

expect(store.filteredCards.map(cardToId)).toEqual([undefined, 3, 4, 5]);

store.cardFilter.sortDirection.onChange("desc");

expect(store.filteredCards.map(cardToId)).toEqual([5, 4, 3, undefined]);
});
});
53 changes: 42 additions & 11 deletions src/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,20 @@ const cardFormToApi = (card: CardFormType) => {
};
};

export type CardFilterSortBy = "createdAt" | "frontAlpha" | "backAlpha";
export type CardFilterDirection = "desc" | "asc";

export class DeckFormStore {
cardFormIndex?: number;
cardFormType?: "new" | "edit";
form?: DeckFormType;
isSending = false;
isCardList = false;
cardFilter = new TextField("");
cardFilter = {
text: new TextField(""),
sortBy: new TextField<CardFilterSortBy>("createdAt"),
sortDirection: new TextField<CardFilterDirection>("desc"),
};

get isDeckSaveButtonVisible() {
return Boolean(
Expand Down Expand Up @@ -104,17 +111,41 @@ export class DeckFormStore {
return [];
}

if (this.cardFilter.value) {
const filterLowerCased = this.cardFilter.value.toLowerCase();
return this.form.cards.filter((card) => {
return (
fuzzySearch(filterLowerCased, card.front.value.toLowerCase()) ||
fuzzySearch(filterLowerCased, card.back.value.toLowerCase())
);
return this.form.cards
.filter((card) => {
if (this.cardFilter.text.value) {
const textFilter = this.cardFilter.text.value.toLowerCase();
return (
fuzzySearch(textFilter, card.front.value.toLowerCase()) ||
fuzzySearch(textFilter, card.back.value.toLowerCase())
);
}
return true;
})
.sort((a, b) => {
if (this.cardFilter.sortBy.value === "frontAlpha") {
return this.cardFilter.sortDirection.value === "desc"
? b.front.value.localeCompare(a.front.value)
: a.front.value.localeCompare(b.front.value);
}
if (this.cardFilter.sortBy.value === "backAlpha") {
return this.cardFilter.sortDirection.value === "desc"
? b.back.value.localeCompare(a.back.value)
: a.back.value.localeCompare(b.back.value);
}
if (this.cardFilter.sortBy.value === "createdAt") {
if (this.cardFilter.sortDirection.value === "desc") {
if (!b.id) return -1;
if (!a.id) return 1;
return b.id - a.id;
}
if (!b.id) return 1;
if (!a.id) return -1;
return a.id - b.id;
}

return this.cardFilter.sortBy.value satisfies never;
});
}

return this.form.cards;
}

get deckFormScreen() {
Expand Down
2 changes: 1 addition & 1 deletion src/store/quick-add-card-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { action, makeAutoObservable } from "mobx";
import {
isFormEmpty,
isFormTouched,
isFormValid
isFormValid,
} from "../lib/mobx-form/form-has-error.ts";
import { screenStore } from "./screen-store.ts";
import { showConfirm } from "../lib/telegram/show-confirm.ts";
Expand Down
18 changes: 11 additions & 7 deletions src/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import React from "react";
import { theme } from "./theme.tsx";
import { css } from "@emotion/css";

type Option = {
type Option<T extends string | number> = {
label: string;
value: string;
value: T;
};

type Props = {
type Props<T extends string | number> = {
value: string;
onChange: (newValue: string) => void;
options: Option[];
onChange: (newValue: T) => void;
options: Option<T>[];
};

export const Select = ({ value, onChange, options }: Props) => {
export const Select = <T extends string | number>({
value,
onChange,
options,
}: Props<T>) => {
return (
<select
className={css({
Expand All @@ -27,7 +31,7 @@ export const Select = ({ value, onChange, options }: Props) => {
color: theme.linkColor,
})}
value={value}
onChange={(e) => onChange(e.target.value)}
onChange={(e) => onChange(e.target.value as T)}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
Expand Down

0 comments on commit 4471516

Please sign in to comment.