2026-05-31 18:51:31 +02:00

280 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 dun 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",
};