280 lines
7.2 KiB
TypeScript
280 lines
7.2 KiB
TypeScript
import { PropertyValues, css, html, nothing } from "lit";
|
||
import { dataStore } from "../services/DataStore.js";
|
||
import { scheduler } from "../services/Scheduler.js";
|
||
import { FLitElement } from "../shared/FLitElement.js";
|
||
import { globalStyle } from "../shared/globalStyle.js";
|
||
import { redirectToHome } from "../shared/navigation.js";
|
||
import { pageStyle } from "../shared/pageStyle.js";
|
||
import { Card, Deck, DeckFrequency } from "../types/data.types.js";
|
||
import { createRandomId } from "../utils/utils.js";
|
||
import { newCardBack, newCardFront } from "./FCardPage.js";
|
||
import { MenuButton } from "./FMenu.js";
|
||
import { toTrainParams } from "./FTrainPage.js";
|
||
|
||
export class FDeckPage extends FLitElement {
|
||
static styles = [
|
||
globalStyle,
|
||
pageStyle,
|
||
css`
|
||
:host {
|
||
.deck {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rem;
|
||
}
|
||
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
h2 {
|
||
flex-grow: 1;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.cards > * {
|
||
aspect-ratio: 2 / 3;
|
||
}
|
||
|
||
.new-card-button {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 5rem;
|
||
border: var(--big-border-thickness) dashed var(--border-color);
|
||
background-color: var(--surface-color);
|
||
}
|
||
|
||
dialog {
|
||
width: 90vw;
|
||
max-width: 40rem;
|
||
/* avoid jumps when the height dynamically changes */
|
||
margin-top: 20vh;
|
||
|
||
section {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
label {
|
||
font-style: italic;
|
||
}
|
||
|
||
p {
|
||
font-style: italic;
|
||
font-size: 1.4rem;
|
||
}
|
||
}
|
||
}
|
||
`,
|
||
];
|
||
|
||
declare deckId: string;
|
||
declare frequency: DeckFrequency;
|
||
declare toTrain: Card[];
|
||
static properties = {
|
||
deckId: { type: Object },
|
||
frequency: { type: String },
|
||
toTrain: { type: Array },
|
||
};
|
||
|
||
#deck!: Deck;
|
||
#cards!: Card[];
|
||
#settingsDialog!: HTMLDialogElement;
|
||
|
||
willUpdate(changedProperties: PropertyValues<this>) {
|
||
if (changedProperties.has("deckId")) {
|
||
const deck = dataStore.getDeck(this.deckId);
|
||
if (!deck) {
|
||
console.error(`deck ${this.deckId} not found`);
|
||
redirectToHome();
|
||
return;
|
||
}
|
||
this.#deck = deck;
|
||
this.frequency = deck.frequency;
|
||
this.#cards = dataStore.getCardsFor(this.deckId);
|
||
this.#setToTrain();
|
||
}
|
||
}
|
||
|
||
onTabVisible() {
|
||
this.#setToTrain();
|
||
}
|
||
|
||
render() {
|
||
const navSettings = html`
|
||
<button
|
||
@click=${() => this.#settingsDialog.showModal()}
|
||
class="symbol-button primary nav-button"
|
||
>
|
||
<span>paramètres du paquet</span>
|
||
<f-template-outlet data-template="icon-settings"></f-template-outlet>
|
||
</button>
|
||
`;
|
||
const navDelete = html`<f-confirm-dialog
|
||
@confirm=${() => this.#deleteDeck()}
|
||
.text=${"supprimer le paquet ?"}
|
||
>
|
||
<button
|
||
class="symbol-button danger nav-button"
|
||
title="supprimer le paquet"
|
||
>
|
||
<span>supprimer le paquet</span>
|
||
<f-template-outlet data-template="icon-delete"></f-template-outlet>
|
||
</button>
|
||
</f-confirm-dialog>`;
|
||
const menu: MenuButton[] = [
|
||
{ type: "home" },
|
||
{ type: "back", url: "/" },
|
||
{ type: "custom", template: navSettings },
|
||
{ type: "custom", template: navDelete },
|
||
];
|
||
return html`
|
||
<f-menu .buttons=${menu}></f-menu>
|
||
<div class="deck">
|
||
<header>
|
||
<h2 class="card">
|
||
<input
|
||
type="text"
|
||
.value=${this.#deck.name}
|
||
@input=${this.#onInputName}
|
||
@focus=${this.#onFocusName}
|
||
size="1"
|
||
/>
|
||
</h2>
|
||
</header>
|
||
${this.toTrain.length > 0
|
||
? html`<a
|
||
href="/train?${toTrainParams(
|
||
this.toTrain.map((card) => card.id),
|
||
)}"
|
||
class="text-button accent"
|
||
>
|
||
réviser
|
||
</a>`
|
||
: nothing}
|
||
<div data-cards class="cards">
|
||
<div>
|
||
<button
|
||
@click=${this.#createCard}
|
||
class="new-card-button"
|
||
title="nouvelle carte"
|
||
>
|
||
<span>+</span>
|
||
</button>
|
||
</div>
|
||
${this.#cards.map(
|
||
(card) => html`<f-card-thumbnail .card=${card}></f-card-thumbnail>`,
|
||
)}
|
||
</div>
|
||
</div>
|
||
<dialog closedby="any">
|
||
<h2>paramètres du paquet</h2>
|
||
<section>
|
||
<div class="field">
|
||
<label for="f-deck-page-range">fréquence</label>
|
||
<input
|
||
type="range"
|
||
value="${frequencyRange[this.#deck.frequency]}"
|
||
@input=${this.#onInputFrequency}
|
||
min="1"
|
||
max="3"
|
||
id="f-deck-page-range"
|
||
list="f-deck-page-range-list"
|
||
/>
|
||
<datalist id="f-deck-page-range-list">
|
||
<option value="1"></option>
|
||
<option value="2"></option>
|
||
<option value="3"></option>
|
||
</datalist>
|
||
</div>
|
||
<p>
|
||
${this.frequency === "low"
|
||
? "révisions moins fréquentes, intervalle maximum d’un an"
|
||
: ""}
|
||
${this.frequency === "medium"
|
||
? "révisions moyennement fréquentes, intervalle maximum de 6 mois"
|
||
: ""}
|
||
${this.frequency === "high"
|
||
? "révisions fréquentes, intervalle maximum de 3 mois"
|
||
: ""}
|
||
</p>
|
||
</section>
|
||
</dialog>
|
||
`;
|
||
}
|
||
|
||
firstUpdated() {
|
||
this.#settingsDialog = this.renderRoot.querySelector("dialog")!;
|
||
}
|
||
|
||
#setToTrain() {
|
||
this.toTrain = scheduler.getTrainingStack(this.#cards);
|
||
}
|
||
|
||
#onFocusName(e: Event) {
|
||
const input = e.target as HTMLInputElement;
|
||
if (input.value === newDeckName) {
|
||
input.select();
|
||
}
|
||
}
|
||
|
||
#onInputName(e: Event) {
|
||
const name = (e.target as HTMLInputElement).value;
|
||
dataStore.editDeck(this.deckId, { name });
|
||
}
|
||
|
||
#createCard() {
|
||
const newCard = {
|
||
id: createRandomId(),
|
||
deckId: this.deckId,
|
||
front: newCardFront,
|
||
back: newCardBack,
|
||
scheduling: scheduler.newScheduling(),
|
||
};
|
||
dataStore.addCard(newCard);
|
||
window.navigation.navigate(`/card/${newCard.id}`);
|
||
}
|
||
|
||
#deleteDeck() {
|
||
dataStore.removeDeck(this.deckId);
|
||
redirectToHome();
|
||
}
|
||
|
||
#onInputFrequency(e: InputEvent) {
|
||
const range = (e.target as HTMLInputElement).value;
|
||
this.frequency = Object.entries(frequencyRange).find(
|
||
([, val]) => val === range,
|
||
)![0] as DeckFrequency;
|
||
dataStore.editDeck(this.#deck.id, { frequency: this.frequency });
|
||
}
|
||
}
|
||
|
||
declare global {
|
||
interface HTMLElementTagNameMap {
|
||
"f-deck-page": FDeckPage;
|
||
}
|
||
}
|
||
|
||
export const newDeckName = "nouveau paquet";
|
||
|
||
const frequencyRange: Record<DeckFrequency, string> = {
|
||
low: "1",
|
||
medium: "2",
|
||
high: "3",
|
||
};
|