@studiotitta/studio-editori 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/komponentit/Editori.d.ts +1 -0
- package/dist/komponentit/Editori.js +29 -0
- package/dist/komponentit/Muokattava.d.ts +10 -0
- package/dist/komponentit/Muokattava.js +7 -0
- package/dist/komponentit/index.d.ts +2 -0
- package/dist/komponentit/index.js +5 -0
- package/dist/maarittely.d.ts +215 -0
- package/dist/maarittely.js +170 -0
- package/dist/palvelin/apurit.d.ts +26 -0
- package/dist/palvelin/apurit.js +70 -0
- package/dist/palvelin/asetukset.d.ts +26 -0
- package/dist/palvelin/asetukset.js +55 -0
- package/dist/palvelin/github/avain.d.ts +15 -0
- package/dist/palvelin/github/avain.js +121 -0
- package/dist/palvelin/github/tiedostot.d.ts +56 -0
- package/dist/palvelin/github/tiedostot.js +144 -0
- package/dist/palvelin/github/yhteys.d.ts +28 -0
- package/dist/palvelin/github/yhteys.js +89 -0
- package/dist/palvelin/index.d.ts +15 -0
- package/dist/palvelin/index.js +19 -0
- package/dist/palvelin/istunto.d.ts +34 -0
- package/dist/palvelin/istunto.js +86 -0
- package/dist/palvelin/kaannos.d.ts +12 -0
- package/dist/palvelin/kaannos.js +102 -0
- package/dist/palvelin/kaannosreitti.d.ts +9 -0
- package/dist/palvelin/kaannosreitti.js +42 -0
- package/dist/palvelin/kirjautuminen.d.ts +25 -0
- package/dist/palvelin/kirjautuminen.js +183 -0
- package/dist/palvelin/kuvat.d.ts +29 -0
- package/dist/palvelin/kuvat.js +130 -0
- package/dist/palvelin/kuvateksti.d.ts +15 -0
- package/dist/palvelin/kuvateksti.js +95 -0
- package/dist/palvelin/lue.d.ts +6 -0
- package/dist/palvelin/lue.js +41 -0
- package/dist/palvelin/posti.d.ts +9 -0
- package/dist/palvelin/posti.js +45 -0
- package/dist/palvelin/reitit.d.ts +10 -0
- package/dist/palvelin/reitit.js +71 -0
- package/dist/palvelin/tallennus.d.ts +18 -0
- package/dist/palvelin/tallennus.js +177 -0
- package/dist/palvelin/tila.d.ts +21 -0
- package/dist/palvelin/tila.js +116 -0
- package/dist/palvelin/tunnus.d.ts +30 -0
- package/dist/palvelin/tunnus.js +84 -0
- package/dist/palvelin/varasto.d.ts +18 -0
- package/dist/palvelin/varasto.js +87 -0
- package/dist/selain/EditorinPalkki.d.ts +1 -0
- package/dist/selain/EditorinPalkki.js +35 -0
- package/dist/selain/Muokkaustila.d.ts +3 -0
- package/dist/selain/Muokkaustila.js +646 -0
- package/dist/selain/PalkkiLataaja.d.ts +1 -0
- package/dist/selain/PalkkiLataaja.js +19 -0
- package/dist/selain/Paneeli.d.ts +44 -0
- package/dist/selain/Paneeli.js +259 -0
- package/dist/selain/index.d.ts +4 -0
- package/dist/selain/index.js +8 -0
- package/dist/selain/kieli.d.ts +10 -0
- package/dist/selain/kieli.js +14 -0
- package/dist/selain/kuva.d.ts +7 -0
- package/dist/selain/kuva.js +45 -0
- package/dist/selain/polku.d.ts +21 -0
- package/dist/selain/polku.js +108 -0
- package/dist/selain/tyypit.d.ts +22 -0
- package/dist/selain/tyypit.js +2 -0
- package/dist/tarkistus.d.ts +26 -0
- package/dist/tarkistus.js +324 -0
- package/package.json +59 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Kentta } from "../maarittely.ts";
|
|
2
|
+
import { asetaArvo, type Kentanpolku } from "./polku.ts";
|
|
3
|
+
import type { SivunMalli } from "./tyypit.ts";
|
|
4
|
+
export interface PaneelinTiedot {
|
|
5
|
+
/** Lataa kuvan; palauttaa osoitteen ja mahdollisen tekstiehdotuksen. */
|
|
6
|
+
lataaKuva(tiedosto: File, maaritys: Extract<Kentta, {
|
|
7
|
+
tyyppi: "kuva";
|
|
8
|
+
}>): Promise<{
|
|
9
|
+
src: string;
|
|
10
|
+
ehdotus: string | null;
|
|
11
|
+
}>;
|
|
12
|
+
sivu: string;
|
|
13
|
+
malli: SivunMalli;
|
|
14
|
+
data: Record<string, unknown>;
|
|
15
|
+
kieli: string;
|
|
16
|
+
/** Sivuston kaikki kielet. Kielivalinta näytetään vain, jos niitä on monta. */
|
|
17
|
+
kielet: string[];
|
|
18
|
+
/** Kentät, jotka näkyvät jo sivulla — ne muokataan paikallaan. */
|
|
19
|
+
sivullaNakyvat: Set<string>;
|
|
20
|
+
/** Valittu kohde kohdelistassa, jos sivulla on sellainen. */
|
|
21
|
+
valittu?: string;
|
|
22
|
+
/** Palvelimen hylkäämät kentät viimeisimmästä tallennuksesta. */
|
|
23
|
+
virheet?: {
|
|
24
|
+
kohde: string;
|
|
25
|
+
viesti: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** Täyttää puuttuvat käännökset ehdotuksina; palauttaa tilaviestin. */
|
|
28
|
+
kaanna(mihin: "sivu" | "kohde"): Promise<string>;
|
|
29
|
+
/** Mitkä kentät odottavat käännöstä: sivun omat ja valitun kohteen. */
|
|
30
|
+
kaannettavia: {
|
|
31
|
+
sivu: string[];
|
|
32
|
+
kohde: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Ovatko käännösehdotukset käytössä tällä sivustolla. */
|
|
35
|
+
kaannosKaytossa: boolean;
|
|
36
|
+
muuta(polku: Kentanpolku, arvo: unknown, kaannettava: boolean): void;
|
|
37
|
+
valitse(tunnus: string): void;
|
|
38
|
+
lisaaKohde(kentta: string, maaritys: Extract<Kentta, {
|
|
39
|
+
tyyppi: "kohdelista";
|
|
40
|
+
}>): void;
|
|
41
|
+
poistaKohde(kentta: string, tunnusKentta: string, tunnus: string): void;
|
|
42
|
+
}
|
|
43
|
+
export declare function Paneeli(tiedot: PaneelinTiedot): import("react").JSX.Element;
|
|
44
|
+
export { asetaArvo };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* ==========================================================================
|
|
4
|
+
SIVUPANEELI
|
|
5
|
+
==========================================================================
|
|
6
|
+
Paneeli on sitä varten, mikä ei näy sivulla: ryhmäkoko, varatut paikat,
|
|
7
|
+
kurssin tila, näkyvyys ja hakukonetekstit. Sivulla näkyvät tekstit
|
|
8
|
+
muokataan paikallaan, ei täällä.
|
|
9
|
+
|
|
10
|
+
Paneeli rakentuu sisältömallista, jonka palvelin lähettää. Editoriin ei
|
|
11
|
+
siis kovakoodata mitään sivustokohtaista: kun sivusto lisää kentän, se
|
|
12
|
+
ilmestyy paneeliin oikeana säätimenä.
|
|
13
|
+
========================================================================== */
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { asetaArvo, lueArvo, vapaaTunnus } from "./polku.js";
|
|
16
|
+
function onOlio(arvo) {
|
|
17
|
+
return typeof arvo === "object" && arvo !== null && !Array.isArray(arvo);
|
|
18
|
+
}
|
|
19
|
+
/** Kentän arvo muokattavassa muodossa: käännettävästä vain nykyinen kieli. */
|
|
20
|
+
function nykyinenArvo(maaritys, arvo, kieli) {
|
|
21
|
+
if ("kaannettava" in maaritys && maaritys.kaannettava) {
|
|
22
|
+
return onOlio(arvo) ? arvo[kieli] : undefined;
|
|
23
|
+
}
|
|
24
|
+
return arvo;
|
|
25
|
+
}
|
|
26
|
+
const KIELTEN_NIMET = {
|
|
27
|
+
fi: "suomeksi",
|
|
28
|
+
en: "englanniksi",
|
|
29
|
+
sv: "ruotsiksi",
|
|
30
|
+
};
|
|
31
|
+
function Sisalto({ maaritys, arvo, tunniste, muuta, lataa, kielet, }) {
|
|
32
|
+
switch (maaritys.tyyppi) {
|
|
33
|
+
case "luku":
|
|
34
|
+
return (_jsx("input", { id: tunniste, type: "number", inputMode: "numeric", value: typeof arvo === "number" ? arvo : "", min: maaritys.pienin, max: maaritys.suurin, onChange: (tapahtuma) => {
|
|
35
|
+
const teksti = tapahtuma.target.value;
|
|
36
|
+
muuta(teksti === "" ? undefined : Number(teksti));
|
|
37
|
+
} }));
|
|
38
|
+
case "valinta":
|
|
39
|
+
return (_jsx("select", { id: tunniste, value: typeof arvo === "string" ? arvo : "", onChange: (tapahtuma) => muuta(tapahtuma.target.value), children: maaritys.vaihtoehdot.map((vaihtoehto) => (_jsx("option", { value: vaihtoehto.arvo, children: vaihtoehto.nimi }, vaihtoehto.arvo))) }));
|
|
40
|
+
case "kytkin":
|
|
41
|
+
return (_jsxs("label", { className: "kytkin", children: [_jsx("input", { id: tunniste, type: "checkbox", checked: arvo === true, onChange: (tapahtuma) => muuta(tapahtuma.target.checked) }), _jsx("span", { children: maaritys.teksti ?? maaritys.nimi ?? "Kyllä" })] }));
|
|
42
|
+
case "aika": {
|
|
43
|
+
const teksti = typeof arvo === "string" ? arvo : "";
|
|
44
|
+
/* datetime-local tarvitsee muodon 2026-11-15T09:00 ilman aikavyöhykettä. */
|
|
45
|
+
const paikallinen = teksti.slice(0, maaritys.kellonaika ? 16 : 10);
|
|
46
|
+
return (_jsx("input", { id: tunniste, type: maaritys.kellonaika ? "datetime-local" : "date", value: paikallinen, onChange: (tapahtuma) => muuta(tapahtuma.target.value) }));
|
|
47
|
+
}
|
|
48
|
+
case "tekstialue":
|
|
49
|
+
return (_jsx("textarea", { id: tunniste, rows: 4, value: typeof arvo === "string" ? arvo : "", onChange: (tapahtuma) => muuta(tapahtuma.target.value) }));
|
|
50
|
+
case "tekstilista":
|
|
51
|
+
return (_jsx("textarea", { id: tunniste, rows: 5, value: Array.isArray(arvo) ? arvo.join("\n\n") : "", onChange: (tapahtuma) => muuta(tapahtuma.target.value
|
|
52
|
+
.split(/\n{2,}/)
|
|
53
|
+
.map((kohta) => kohta.trim())
|
|
54
|
+
.filter((kohta) => kohta !== "")) }));
|
|
55
|
+
case "kielivalinta": {
|
|
56
|
+
const valitut = Array.isArray(arvo) ? arvo : [];
|
|
57
|
+
return (_jsxs("div", { className: "kielivalinta", children: [kielet.map((kieli) => (_jsxs("label", { className: "kytkin", children: [_jsx("input", { type: "checkbox", checked: valitut.includes(kieli), onChange: (tapahtuma) => muuta(tapahtuma.target.checked
|
|
58
|
+
? [...kielet.filter((muu) => valitut.includes(muu) || muu === kieli)]
|
|
59
|
+
: valitut.filter((muu) => muu !== kieli)) }), _jsxs("span", { children: ["N\u00E4yt\u00E4 ", KIELTEN_NIMET[kieli] ?? kieli] })] }, kieli))), valitut.length === 0 ? (_jsx("p", { className: "huomio", children: "Kohde ei n\u00E4y mill\u00E4\u00E4n kielell\u00E4." })) : null] }));
|
|
60
|
+
}
|
|
61
|
+
case "kuva":
|
|
62
|
+
return _jsx(Kuvakentta, { maaritys: maaritys, arvo: arvo, muuta: muuta, lataa: lataa });
|
|
63
|
+
default:
|
|
64
|
+
return (_jsx("input", { id: tunniste, type: "text", value: typeof arvo === "string" ? arvo : "", onChange: (tapahtuma) => muuta(tapahtuma.target.value) }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function Kentta_({ polku, maaritys, tiedot, }) {
|
|
68
|
+
const nimi = maaritys.nimi ?? polku.alikentta?.split(".").at(-1) ?? polku.kentta;
|
|
69
|
+
const tunniste = `kentta-${polku.kentta}-${polku.tunnus ?? ""}-${(polku.alikentta ?? "").replace(".", "-")}`;
|
|
70
|
+
const kaannettava = "kaannettava" in maaritys && maaritys.kaannettava === true;
|
|
71
|
+
const arvo = nykyinenArvo(maaritys, lueArvo(tiedot.data, polku), tiedot.kieli);
|
|
72
|
+
return (_jsxs("div", { className: "kentta", children: [_jsxs("label", { htmlFor: tunniste, children: [nimi, kaannettava ? _jsx("span", { className: "kieli", children: tiedot.kieli }) : null] }), _jsx(Sisalto, { maaritys: maaritys, arvo: arvo, tunniste: tunniste, kielet: tiedot.kielet, muuta: (uusi) => tiedot.muuta(polku, uusi, kaannettava), lataa: maaritys.tyyppi === "kuva"
|
|
73
|
+
? async (tiedosto) => {
|
|
74
|
+
const { src, ehdotus } = await tiedot.lataaKuva(tiedosto, maaritys);
|
|
75
|
+
const vanha = onOlio(arvo) ? arvo : {};
|
|
76
|
+
/* Tyhjä teksti täytetään ehdotuksella suoraan; olemassa
|
|
77
|
+
olevaa ei ylikirjoiteta, vaan ehdotus tarjotaan. */
|
|
78
|
+
const alt = typeof vanha.alt === "string" ? vanha.alt.trim() : "";
|
|
79
|
+
tiedot.muuta(polku, { ...vanha, src, ...(alt === "" && ehdotus ? { alt: ehdotus } : {}) }, false);
|
|
80
|
+
return ehdotus;
|
|
81
|
+
}
|
|
82
|
+
: undefined }), _jsx(Vapaana, { polku: polku, maaritys: maaritys, arvo: arvo, tiedot: tiedot }), maaritys.ohje ? _jsx("p", { className: "ohje", children: maaritys.ohje }) : null] }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Vapaat paikat laskettuna: ryhmäkoko miinus varatut.
|
|
86
|
+
*
|
|
87
|
+
* Tämä näkyy vain muokkaajalle. Kävijälle kerrotaan enintään ryhmäkoko —
|
|
88
|
+
* se, montako on jo ilmoittautunut, on tietoisesti hänen tietonsa
|
|
89
|
+
* ulkopuolella.
|
|
90
|
+
*/
|
|
91
|
+
function Vapaana({ polku, maaritys, arvo, tiedot, }) {
|
|
92
|
+
if (maaritys.tyyppi !== "luku" || !maaritys.enintaanKuin)
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof arvo !== "number")
|
|
95
|
+
return null;
|
|
96
|
+
const yla = lueArvo(tiedot.data, { ...polku, alikentta: maaritys.enintaanKuin });
|
|
97
|
+
if (typeof yla !== "number")
|
|
98
|
+
return null;
|
|
99
|
+
const vapaana = Math.max(0, yla - arvo);
|
|
100
|
+
return (_jsx("p", { className: "laskuri", role: "status", children: vapaana === 0 ? "Täynnä" : `Vapaana ${vapaana} / ${yla} paikkaa` }));
|
|
101
|
+
}
|
|
102
|
+
export function Paneeli(tiedot) {
|
|
103
|
+
const kentat = Object.entries(tiedot.malli.kentat);
|
|
104
|
+
const lista = kentat.find(([, maaritys]) => maaritys.tyyppi === "kohdelista");
|
|
105
|
+
const nayta = ([avain, maaritys]) => {
|
|
106
|
+
/* Kielivalinta on turha, jos sivustolla on vain yksi kieli. */
|
|
107
|
+
if (maaritys.tyyppi === "kielivalinta" && tiedot.kielet.length < 2)
|
|
108
|
+
return false;
|
|
109
|
+
return !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}`);
|
|
110
|
+
};
|
|
111
|
+
const omat = kentat.filter(([avain, maaritys]) => maaritys.tyyppi !== "kohdelista" && nayta([avain, maaritys]));
|
|
112
|
+
return (_jsxs("aside", { className: "paneeli", "aria-label": "Sis\u00E4ll\u00F6n tiedot", children: [tiedot.virheet && tiedot.virheet.length > 0 ? (_jsxs("section", { className: "virheet", role: "alert", children: [_jsx("h2", { children: "Tallennus pys\u00E4htyi" }), _jsx("ul", { children: tiedot.virheet.slice(0, 6).map((virhe) => (_jsxs("li", { children: [_jsx("strong", { children: virhe.kohde.split(".").at(-1) }), " ", virhe.viesti] }, `${virhe.kohde}-${virhe.viesti}`))) })] })) : null, omat.length > 0 || tiedot.kaannettavia.sivu.length > 0 ? (_jsxs("section", { children: [_jsx("h2", { children: tiedot.malli.nimi }), omat.map(([avain, maaritys]) => (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain }, maaritys: maaritys, tiedot: tiedot }, avain))), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "sivu" }) : null] })) : null, lista ? _jsx(Kohteet, { avain: lista[0], maaritys: lista[1], tiedot: tiedot }) : null] }));
|
|
113
|
+
}
|
|
114
|
+
const KIELELLE = {
|
|
115
|
+
fi: "suomeksi",
|
|
116
|
+
en: "englanniksi",
|
|
117
|
+
sv: "ruotsiksi",
|
|
118
|
+
};
|
|
119
|
+
const KIELINEN = {
|
|
120
|
+
fi: "Suomenkielinen",
|
|
121
|
+
en: "Englanninkielinen",
|
|
122
|
+
sv: "Ruotsinkielinen",
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Käännösehdotukset.
|
|
126
|
+
*
|
|
127
|
+
* Painike täyttää vain tyhjät kohdat, eikä koskaan kirjoita valmiin
|
|
128
|
+
* käännöksen päälle. Ehdotukset jäävät kenttiin luettaviksi — tallennus on
|
|
129
|
+
* yhä oma painalluksensa.
|
|
130
|
+
*/
|
|
131
|
+
function Kaannos({ tiedot, mihin }) {
|
|
132
|
+
const [tila, setTila] = useState("valmis");
|
|
133
|
+
const [viesti, setViesti] = useState("");
|
|
134
|
+
const puuttuvat = tiedot.kaannettavia[mihin];
|
|
135
|
+
if (puuttuvat.length === 0 && viesti === "")
|
|
136
|
+
return null;
|
|
137
|
+
const kielella = KIELELLE[tiedot.kieli] ?? tiedot.kieli;
|
|
138
|
+
const kieli = KIELINEN[tiedot.kieli] ?? tiedot.kieli;
|
|
139
|
+
/* Kentät nimeltä: "puuttuu yhdestä kohdasta" ei kerro, onko kyse
|
|
140
|
+
otsikosta vai koko esittelystä. */
|
|
141
|
+
const nimet = puuttuvat.slice(0, 4).join(", ");
|
|
142
|
+
const loput = puuttuvat.length - 4;
|
|
143
|
+
return (_jsxs("div", { className: "kaannos", children: [puuttuvat.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("p", { children: [kieli, " k\u00E4\u00E4nn\u00F6s puuttuu ", puuttuvat.length === 1 ? "kentästä" : "kentistä", ":", _jsxs("strong", { children: [nimet, loput > 0 ? ` ja ${loput} muuta` : ""] })] }), tiedot.kaannosKaytossa ? (_jsx("button", { type: "button", disabled: tila === "kaantaa", onClick: async () => {
|
|
144
|
+
setTila("kaantaa");
|
|
145
|
+
setViesti("");
|
|
146
|
+
setViesti(await tiedot.kaanna(mihin));
|
|
147
|
+
setTila("valmis");
|
|
148
|
+
}, children: tila === "kaantaa" ? "Käännetään…" : `Käännä ${kielella}` })) : null] })) : null, viesti ? (_jsx("p", { className: "ohje", role: "status", children: viesti })) : null] }));
|
|
149
|
+
}
|
|
150
|
+
function Kohteet({ avain, maaritys, tiedot, }) {
|
|
151
|
+
if (maaritys.tyyppi !== "kohdelista")
|
|
152
|
+
return null;
|
|
153
|
+
const kohteet = Array.isArray(tiedot.data[avain])
|
|
154
|
+
? tiedot.data[avain]
|
|
155
|
+
: [];
|
|
156
|
+
const nimi = (kohde) => {
|
|
157
|
+
const arvo = kohde[maaritys.otsikko];
|
|
158
|
+
if (typeof arvo === "string")
|
|
159
|
+
return arvo === "" ? "(nimetön)" : arvo;
|
|
160
|
+
if (!onOlio(arvo))
|
|
161
|
+
return "(nimetön)";
|
|
162
|
+
/* Nykyinen kieli ensin, mutta kääntämätön kurssi näkyy listassa sillä
|
|
163
|
+
kielellä, jolla se on kirjoitettu — ei nimettömänä. */
|
|
164
|
+
const omalla = arvo[tiedot.kieli];
|
|
165
|
+
if (typeof omalla === "string" && omalla !== "")
|
|
166
|
+
return omalla;
|
|
167
|
+
const joku = Object.values(arvo).find((muu) => typeof muu === "string" && muu !== "");
|
|
168
|
+
return joku ?? "(nimetön)";
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Merkinnät kohteen nimen perässä: piilotus, huomionarvoinen tila ja
|
|
172
|
+
* täyttyminen. Näin listasta näkee tilanteen avaamatta jokaista kohdetta.
|
|
173
|
+
*/
|
|
174
|
+
const merkinnat = (kohde) => {
|
|
175
|
+
const tulos = [];
|
|
176
|
+
for (const [alikentta, kenttä] of Object.entries(maaritys.kentat)) {
|
|
177
|
+
if (kenttä.tyyppi === "kytkin" && kenttä.piilottaa && kohde[alikentta] === true) {
|
|
178
|
+
tulos.push("piilossa");
|
|
179
|
+
}
|
|
180
|
+
if (kenttä.tyyppi === "valinta") {
|
|
181
|
+
const valittu = kenttä.vaihtoehdot.find((vaihtoehto) => vaihtoehto.arvo === kohde[alikentta]);
|
|
182
|
+
if (valittu?.merkitse)
|
|
183
|
+
tulos.push(valittu.nimi.toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
/* Täyttyminen lasketaan, sitä ei ole kenttänä missään. */
|
|
186
|
+
if (kenttä.tyyppi === "luku" && kenttä.enintaanKuin) {
|
|
187
|
+
const varattu = kohde[alikentta];
|
|
188
|
+
const yla = kohde[kenttä.enintaanKuin];
|
|
189
|
+
if (typeof varattu === "number" && typeof yla === "number" && yla > 0 && varattu >= yla) {
|
|
190
|
+
tulos.push("täynnä");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return tulos;
|
|
195
|
+
};
|
|
196
|
+
const yksikko = maaritys.yksikko ?? "kohde";
|
|
197
|
+
const valittu = kohteet.find((kohde) => kohde[maaritys.tunnus] === tiedot.valittu);
|
|
198
|
+
const valitunTunnus = typeof valittu?.[maaritys.tunnus] === "string" ? valittu[maaritys.tunnus] : undefined;
|
|
199
|
+
const alikentat = Object.entries(maaritys.kentat).filter(([alikentta, kenttä]) => alikentta !== maaritys.tunnus &&
|
|
200
|
+
!(kenttä.tyyppi === "kielivalinta" && tiedot.kielet.length < 2) &&
|
|
201
|
+
!tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}[${valitunTunnus}].${alikentta}`));
|
|
202
|
+
/* Usein muuttuvat kentät esiin, kerran asetettavat taitoksen alle. */
|
|
203
|
+
const ensisijaiset = alikentat.filter(([, kenttä]) => kenttä.lisatieto !== true);
|
|
204
|
+
const lisatiedot = alikentat.filter(([, kenttä]) => kenttä.lisatieto === true);
|
|
205
|
+
const kentta = ([alikentta, kenttä]) => kenttä.tyyppi === "ryhma" ? (_jsxs("div", { className: "ryhma", children: [_jsx("h3", { children: kenttä.nimi ?? alikentta }), kenttä.ohje ? _jsx("p", { className: "ohje", children: kenttä.ohje }) : null, Object.entries(kenttä.kentat).map(([ryhmanKentta, ryhmanMaaritys]) => (_jsx(Kentta_, { polku: {
|
|
206
|
+
sivu: tiedot.sivu,
|
|
207
|
+
kentta: avain,
|
|
208
|
+
tunnus: valitunTunnus,
|
|
209
|
+
alikentta: `${alikentta}.${ryhmanKentta}`,
|
|
210
|
+
}, maaritys: ryhmanMaaritys, tiedot: tiedot }, ryhmanKentta)))] }, alikentta)) : (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain, tunnus: valitunTunnus, alikentta }, maaritys: kenttä, tiedot: tiedot }, alikentta));
|
|
211
|
+
return (_jsxs(_Fragment, { children: [_jsxs("section", { children: [_jsx("h2", { children: maaritys.nimi ?? tiedot.malli.nimi }), _jsx("ul", { className: "kohteet", children: kohteet.map((kohde) => {
|
|
212
|
+
const tunnus = String(kohde[maaritys.tunnus] ?? "");
|
|
213
|
+
const omatMerkinnat = merkinnat(kohde);
|
|
214
|
+
return (_jsx("li", { children: _jsxs("button", { type: "button", className: tunnus === valitunTunnus ? "valittu" : undefined, "aria-current": tunnus === valitunTunnus ? "true" : undefined, onClick: () => tiedot.valitse(tunnus), children: [_jsx("span", { children: nimi(kohde) }), omatMerkinnat.length > 0 ? (_jsx("span", { className: "merkki", children: omatMerkinnat.join(" · ") })) : null] }) }, tunnus));
|
|
215
|
+
}) }), _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", onClick: () => tiedot.lisaaKohde(avain, maaritys), children: ["+ Lis\u00E4\u00E4 ", yksikko] }) }), valittu ? null : (_jsxs("p", { className: "ohje", children: ["Valitse ", yksikko, ", niin n\u00E4et sen tiedot."] }))] }), valittu && valitunTunnus ? (_jsxs("section", { children: [_jsx("h2", { children: nimi(valittu) }), ensisijaiset.map(kentta), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "kohde" }) : null, lisatiedot.length > 0 ? (_jsxs("details", { className: "lisatiedot", children: [_jsx("summary", { children: "Lis\u00E4tiedot" }), lisatiedot.map(kentta)] })) : null, _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", className: "vaarallinen", onClick: () => tiedot.poistaKohde(avain, maaritys.tunnus, valitunTunnus), children: ["Poista ", yksikko] }) })] })) : null] }));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Kuvakenttä: nykyinen kuva, vaihto ja vaihtoehtoinen teksti.
|
|
219
|
+
*
|
|
220
|
+
* Kuva lähtee heti valittaessa, koska käsittely ja tallennus kestävät
|
|
221
|
+
* hetken — muokkaaja näkee lopputuloksen ennen kuin painaa Tallenna.
|
|
222
|
+
*/
|
|
223
|
+
function Kuvakentta({ maaritys, arvo, muuta, lataa, }) {
|
|
224
|
+
const [tila, setTila] = useState("valmis");
|
|
225
|
+
const [virhe, setVirhe] = useState("");
|
|
226
|
+
/* Vasta vaihdettu kuva: vanha teksti kuvaa yhä vanhaa kuvaa. */
|
|
227
|
+
const [vaihdettu, setVaihdettu] = useState(false);
|
|
228
|
+
const [ehdotus, setEhdotus] = useState(null);
|
|
229
|
+
const kuva = onOlio(arvo) ? arvo : undefined;
|
|
230
|
+
async function valitse(tiedosto) {
|
|
231
|
+
if (!tiedosto)
|
|
232
|
+
return;
|
|
233
|
+
setTila("lataa");
|
|
234
|
+
setVirhe("");
|
|
235
|
+
setEhdotus(null);
|
|
236
|
+
try {
|
|
237
|
+
const nykyinenAlt = typeof kuva?.alt === "string" ? kuva.alt.trim() : "";
|
|
238
|
+
const uusiEhdotus = await lataa(tiedosto);
|
|
239
|
+
setTila("valmis");
|
|
240
|
+
setVaihdettu(true);
|
|
241
|
+
/* Jos kentässä oli jo teksti, ehdotusta tarjotaan mutta ei tyrkytetä. */
|
|
242
|
+
if (uusiEhdotus && nykyinenAlt !== "")
|
|
243
|
+
setEhdotus(uusiEhdotus);
|
|
244
|
+
}
|
|
245
|
+
catch (syy) {
|
|
246
|
+
setTila("virhe");
|
|
247
|
+
setVirhe(syy instanceof Error ? syy.message : "Kuvan lataus ei onnistunut.");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return (_jsxs("div", { className: "kuva", children: [typeof kuva?.src === "string" ? (_jsx("img", { src: kuva.src, alt: "" })) : (_jsx("p", { className: "tyhja", children: "Ei kuvaa." })), _jsxs("label", { className: "lataus", children: [_jsx("span", { children: tila === "lataa" ? "Käsitellään…" : "Vaihda kuva" }), _jsx("input", { type: "file", accept: "image/*", disabled: tila === "lataa", onChange: (tapahtuma) => valitse(tapahtuma.target.files?.[0]) })] }), tila === "virhe" ? _jsx("p", { className: "virheviesti", children: virhe }) : null, _jsxs("label", { className: "alt", children: ["Kuvan kuvaus", _jsx("input", { type: "text", placeholder: "Esimerkiksi: kynttil\u00E4 ikkunalaudalla", value: typeof kuva?.alt === "string" ? kuva.alt : "", onChange: (tapahtuma) => {
|
|
251
|
+
setVaihdettu(false);
|
|
252
|
+
muuta({ ...(kuva ?? {}), alt: tapahtuma.target.value });
|
|
253
|
+
} })] }), ehdotus ? (_jsxs("div", { className: "huomio", role: "status", children: [_jsxs("p", { children: ["Ehdotus: \u201C", ehdotus, "\u201D"] }), _jsx("button", { type: "button", onClick: () => {
|
|
254
|
+
muuta({ ...(kuva ?? {}), alt: ehdotus });
|
|
255
|
+
setEhdotus(null);
|
|
256
|
+
setVaihdettu(false);
|
|
257
|
+
}, children: "K\u00E4yt\u00E4" })] })) : vaihdettu ? (_jsx("p", { className: "huomio", role: "status", children: "Tarkista kuvan kuvaus." })) : null, maaritys.pakollinenAlt === false ? null : (_jsx("p", { className: "ohje", children: "N\u00E4kyy vain, jos kuva ei lataudu. Ruudunlukija lukee sen \u00E4\u00E4neen. (alt-teksti)" }))] }));
|
|
258
|
+
}
|
|
259
|
+
export { asetaArvo };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Studio Editori — selainosa. Nämä ovat asiakaskomponentteja ("use client"),
|
|
3
|
+
ja ne ladataan vain kirjautuneelle muokkaajalle.
|
|
4
|
+
*/
|
|
5
|
+
export { EditorinPalkki } from "./EditorinPalkki.js";
|
|
6
|
+
export { PalkkiLataaja } from "./PalkkiLataaja.js";
|
|
7
|
+
export { Muokkaustila } from "./Muokkaustila.js";
|
|
8
|
+
export { valitseKieli } from "./kieli.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sivun `lang`-määreestä sivuston kieleksi.
|
|
3
|
+
*
|
|
4
|
+
* Omassa tiedostossaan (ilman JSX:ää), jotta sen voi testata suoraan
|
|
5
|
+
* Nodella ilman selainta tai käännösvaihetta.
|
|
6
|
+
*
|
|
7
|
+
* Tuntematon kieli olisi uusi kenttä sisältötiedostoon, ja palvelin hylkäisi
|
|
8
|
+
* sen — turvallisempi on tallentaa oletuskielelle kuin kadottaa teksti.
|
|
9
|
+
*/
|
|
10
|
+
export declare function valitseKieli(lang: string | null | undefined, kielet: string[]): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sivun `lang`-määreestä sivuston kieleksi.
|
|
3
|
+
*
|
|
4
|
+
* Omassa tiedostossaan (ilman JSX:ää), jotta sen voi testata suoraan
|
|
5
|
+
* Nodella ilman selainta tai käännösvaihetta.
|
|
6
|
+
*
|
|
7
|
+
* Tuntematon kieli olisi uusi kenttä sisältötiedostoon, ja palvelin hylkäisi
|
|
8
|
+
* sen — turvallisempi on tallentaa oletuskielelle kuin kadottaa teksti.
|
|
9
|
+
*/
|
|
10
|
+
export function valitseKieli(lang, kielet) {
|
|
11
|
+
const oletus = kielet[0] ?? "fi";
|
|
12
|
+
const arvo = (lang ?? "").trim().toLowerCase().split("-")[0];
|
|
13
|
+
return arvo && kielet.includes(arvo) ? arvo : oletus;
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
KUVA SELAIMESSA
|
|
3
|
+
==========================================================================
|
|
4
|
+
Kuva piirretään uudelleen ennen lähetystä. Sillä on kolme tarkoitusta:
|
|
5
|
+
|
|
6
|
+
1. Kameran kuva on kymmeniä megatavuja — se ei mene palvelimelle
|
|
7
|
+
sellaisenaan.
|
|
8
|
+
2. Uudelleenpiirto pudottaa metatiedot, myös kuvauspaikan sijainnin.
|
|
9
|
+
3. Palvelimelle ei päädy alkuperäistä tiedostoa lainkaan, joten
|
|
10
|
+
kuvankäsittelykirjastojen aukot eivät ole samalla tavalla alttiina.
|
|
11
|
+
========================================================================== */
|
|
12
|
+
const PISIN_SIVU = 2560;
|
|
13
|
+
export async function pienennaKuva(tiedosto, pisinSivu = PISIN_SIVU) {
|
|
14
|
+
const kuva = await lueKuva(tiedosto);
|
|
15
|
+
const suurin = Math.max(kuva.width, kuva.height);
|
|
16
|
+
const kerroin = suurin > pisinSivu ? pisinSivu / suurin : 1;
|
|
17
|
+
const leveys = Math.round(kuva.width * kerroin);
|
|
18
|
+
const korkeus = Math.round(kuva.height * kerroin);
|
|
19
|
+
const kangas = document.createElement("canvas");
|
|
20
|
+
kangas.width = leveys;
|
|
21
|
+
kangas.height = korkeus;
|
|
22
|
+
const piirtaja = kangas.getContext("2d");
|
|
23
|
+
if (!piirtaja)
|
|
24
|
+
throw new Error("Selain ei osannut käsitellä kuvaa.");
|
|
25
|
+
piirtaja.drawImage(kuva, 0, 0, leveys, korkeus);
|
|
26
|
+
/* JPEG eikä PNG: valokuvista tulisi PNG:nä moninkertaisia. Palvelin
|
|
27
|
+
muuntaa lopullisen muodon joka tapauksessa. */
|
|
28
|
+
const data = kangas.toDataURL("image/jpeg", 0.9);
|
|
29
|
+
return { tavut: data.split(",")[1] ?? "", leveys, korkeus };
|
|
30
|
+
}
|
|
31
|
+
function lueKuva(tiedosto) {
|
|
32
|
+
return new Promise((valmis, virhe) => {
|
|
33
|
+
const osoite = URL.createObjectURL(tiedosto);
|
|
34
|
+
const kuva = new Image();
|
|
35
|
+
kuva.onload = () => {
|
|
36
|
+
URL.revokeObjectURL(osoite);
|
|
37
|
+
valmis(kuva);
|
|
38
|
+
};
|
|
39
|
+
kuva.onerror = () => {
|
|
40
|
+
URL.revokeObjectURL(osoite);
|
|
41
|
+
virhe(new Error("Kuvaa ei voitu avata. Kokeile toista tiedostoa."));
|
|
42
|
+
};
|
|
43
|
+
kuva.src = osoite;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Kentanpolku {
|
|
2
|
+
sivu: string;
|
|
3
|
+
kentta: string;
|
|
4
|
+
/** Kohteen tunnus, jos kenttä on listan sisällä. */
|
|
5
|
+
tunnus?: string;
|
|
6
|
+
/** Kentän nimi kohteen sisällä. */
|
|
7
|
+
alikentta?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function lueKentanpolku(polku: string): Kentanpolku | null;
|
|
10
|
+
export declare function kirjoitaKentanpolku(polku: Kentanpolku): string;
|
|
11
|
+
/**
|
|
12
|
+
* Kirjoittaa yhden arvon sivun sisältöön ja palauttaa uuden sisällön.
|
|
13
|
+
*
|
|
14
|
+
* Alkuperäistä ei muuteta: muokkaus voi epäonnistua tai perua, eikä silloin
|
|
15
|
+
* saa jäädä puolikasta tilaa. `kieli` annetaan vain käännettäville kentille.
|
|
16
|
+
*/
|
|
17
|
+
export declare function asetaArvo(sivunData: Record<string, unknown>, polku: Kentanpolku, arvo: unknown, kieli?: string, tunnusKentta?: string): Record<string, unknown>;
|
|
18
|
+
/** Lukee yhden arvon sivun sisällöstä. */
|
|
19
|
+
export declare function lueArvo(sivunData: Record<string, unknown>, polku: Kentanpolku, tunnusKentta?: string): unknown;
|
|
20
|
+
/** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
|
|
21
|
+
export declare function vapaaTunnus(kaytossa: readonly string[], ehdotus: string): string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
KENTTIEN POLUT
|
|
3
|
+
==========================================================================
|
|
4
|
+
Sivulla oleva merkintä kertoo, mitä kohtaa sisällöstä elementti näyttää:
|
|
5
|
+
|
|
6
|
+
kurssit.otsikko sivun oma kenttä
|
|
7
|
+
kurssit.lista[joogaretriitti].nimi yhden kohteen kenttä listassa
|
|
8
|
+
|
|
9
|
+
Hakasulkeissa on kohteen tunnus, ei järjestysnumero. Näin muokkaus osuu
|
|
10
|
+
oikeaan kurssiin, vaikka järjestys muuttuisi tai joku lisäisi kurssin
|
|
11
|
+
samaan aikaan.
|
|
12
|
+
|
|
13
|
+
Tämä tiedosto on tarkoituksella pelkkää logiikkaa ilman selainta, jotta
|
|
14
|
+
sen voi testata suoraan.
|
|
15
|
+
========================================================================== */
|
|
16
|
+
/* Alikenttä voi olla ryhmän sisällä: "kurssit.lista[tunnus].meta.title". */
|
|
17
|
+
const MUOTO = /^([a-zA-Z][a-zA-Z0-9]*)\.([a-zA-Z][a-zA-Z0-9]*)(?:\[([^\]]+)\]\.([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)?))?$/;
|
|
18
|
+
export function lueKentanpolku(polku) {
|
|
19
|
+
const osuma = MUOTO.exec(polku.trim());
|
|
20
|
+
if (!osuma)
|
|
21
|
+
return null;
|
|
22
|
+
const [, sivu, kentta, tunnus, alikentta] = osuma;
|
|
23
|
+
if (!sivu || !kentta)
|
|
24
|
+
return null;
|
|
25
|
+
if (tunnus !== undefined && !alikentta)
|
|
26
|
+
return null;
|
|
27
|
+
return tunnus === undefined ? { sivu, kentta } : { sivu, kentta, tunnus, alikentta };
|
|
28
|
+
}
|
|
29
|
+
export function kirjoitaKentanpolku(polku) {
|
|
30
|
+
return polku.tunnus === undefined
|
|
31
|
+
? `${polku.sivu}.${polku.kentta}`
|
|
32
|
+
: `${polku.sivu}.${polku.kentta}[${polku.tunnus}].${polku.alikentta}`;
|
|
33
|
+
}
|
|
34
|
+
/* ---- Arvon asettaminen --------------------------------------------------- */
|
|
35
|
+
function onOlio(arvo) {
|
|
36
|
+
return typeof arvo === "object" && arvo !== null && !Array.isArray(arvo);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Kirjoittaa yhden arvon sivun sisältöön ja palauttaa uuden sisällön.
|
|
40
|
+
*
|
|
41
|
+
* Alkuperäistä ei muuteta: muokkaus voi epäonnistua tai perua, eikä silloin
|
|
42
|
+
* saa jäädä puolikasta tilaa. `kieli` annetaan vain käännettäville kentille.
|
|
43
|
+
*/
|
|
44
|
+
export function asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta = "tunnus") {
|
|
45
|
+
const yhdista = (vanha) => {
|
|
46
|
+
if (!kieli)
|
|
47
|
+
return arvo;
|
|
48
|
+
const pohja = onOlio(vanha) ? vanha : {};
|
|
49
|
+
return { ...pohja, [kieli]: arvo };
|
|
50
|
+
};
|
|
51
|
+
if (polku.tunnus === undefined) {
|
|
52
|
+
return { ...sivunData, [polku.kentta]: yhdista(sivunData[polku.kentta]) };
|
|
53
|
+
}
|
|
54
|
+
const lista = Array.isArray(sivunData[polku.kentta]) ? [...sivunData[polku.kentta]] : [];
|
|
55
|
+
const sija = lista.findIndex((kohde) => onOlio(kohde) && kohde[tunnusKentta] === polku.tunnus);
|
|
56
|
+
if (sija === -1)
|
|
57
|
+
return sivunData;
|
|
58
|
+
const kohde = lista[sija];
|
|
59
|
+
const [ensimmainen, ryhmanKentta] = polku.alikentta.split(".");
|
|
60
|
+
const avain = ensimmainen;
|
|
61
|
+
if (ryhmanKentta === undefined) {
|
|
62
|
+
lista[sija] = { ...kohde, [avain]: yhdista(kohde[avain]) };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
/* Ryhmän sisällä oleva kenttä: muu ryhmä säilyy ennallaan. */
|
|
66
|
+
const ryhma = onOlio(kohde[avain]) ? kohde[avain] : {};
|
|
67
|
+
lista[sija] = {
|
|
68
|
+
...kohde,
|
|
69
|
+
[avain]: { ...ryhma, [ryhmanKentta]: yhdista(ryhma[ryhmanKentta]) },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { ...sivunData, [polku.kentta]: lista };
|
|
73
|
+
}
|
|
74
|
+
/** Lukee yhden arvon sivun sisällöstä. */
|
|
75
|
+
export function lueArvo(sivunData, polku, tunnusKentta = "tunnus") {
|
|
76
|
+
if (polku.tunnus === undefined)
|
|
77
|
+
return sivunData[polku.kentta];
|
|
78
|
+
const lista = sivunData[polku.kentta];
|
|
79
|
+
if (!Array.isArray(lista))
|
|
80
|
+
return undefined;
|
|
81
|
+
const kohde = lista.find((osa) => onOlio(osa) && osa[tunnusKentta] === polku.tunnus);
|
|
82
|
+
if (!onOlio(kohde))
|
|
83
|
+
return undefined;
|
|
84
|
+
const [ensimmainen, ryhmanKentta] = polku.alikentta.split(".");
|
|
85
|
+
const arvo = kohde[ensimmainen];
|
|
86
|
+
if (ryhmanKentta === undefined)
|
|
87
|
+
return arvo;
|
|
88
|
+
return onOlio(arvo) ? arvo[ryhmanKentta] : undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
|
|
91
|
+
export function vapaaTunnus(kaytossa, ehdotus) {
|
|
92
|
+
const siisti = ehdotus
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replaceAll("ä", "a")
|
|
95
|
+
.replaceAll("ö", "o")
|
|
96
|
+
.replaceAll("å", "a")
|
|
97
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
98
|
+
.replace(/^-+|-+$/g, "")
|
|
99
|
+
.slice(0, 60) || "uusi";
|
|
100
|
+
if (!kaytossa.includes(siisti))
|
|
101
|
+
return siisti;
|
|
102
|
+
for (let numero = 2; numero < 1000; numero++) {
|
|
103
|
+
const kokeilu = `${siisti}-${numero}`;
|
|
104
|
+
if (!kaytossa.includes(kokeilu))
|
|
105
|
+
return kokeilu;
|
|
106
|
+
}
|
|
107
|
+
return `${siisti}-${Date.now()}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Kentta } from "../maarittely.ts";
|
|
2
|
+
export interface SivunMalli {
|
|
3
|
+
nimi: string;
|
|
4
|
+
kentat: Record<string, Kentta>;
|
|
5
|
+
}
|
|
6
|
+
export interface Sisalto {
|
|
7
|
+
versio: string;
|
|
8
|
+
muokkaaja: string;
|
|
9
|
+
/** Sivun avain → sivun sisältö sellaisena kuin se on tiedostossa. */
|
|
10
|
+
sivut: Record<string, Record<string, unknown>>;
|
|
11
|
+
/** Sivun avain → sen tiedoston versio, josta muokkaus alkoi. */
|
|
12
|
+
versiot: Record<string, string>;
|
|
13
|
+
/** Sivuston kielet, ensimmäinen on oletuskieli. */
|
|
14
|
+
kielet: string[];
|
|
15
|
+
/** Sivun avain → mitä sivulla saa muokata. */
|
|
16
|
+
malli: Record<string, SivunMalli>;
|
|
17
|
+
/** Valinnaiset ominaisuudet, jotka vaativat oman avaimensa. */
|
|
18
|
+
ominaisuudet?: {
|
|
19
|
+
kuvateksti: boolean;
|
|
20
|
+
kaannos: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SisallonMaarittely, Sivu, SivunMaarittely } from "./maarittely.ts";
|
|
2
|
+
export interface Huomio {
|
|
3
|
+
/** Kentän polku, esim. "etusivu.avauskysymys", tai sivu, jos koskee koko sivua. */
|
|
4
|
+
kohde: string;
|
|
5
|
+
/** Kieli, jos huomio koskee yhtä kieliversiota. */
|
|
6
|
+
kieli?: string;
|
|
7
|
+
viesti: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Tarkistustulos {
|
|
10
|
+
virheet: Huomio[];
|
|
11
|
+
varoitukset: Huomio[];
|
|
12
|
+
}
|
|
13
|
+
/** Merkkien määrä niin kuin lukija ne näkee (esim. "é" tai emoji = 1). */
|
|
14
|
+
export declare function merkkimaara(teksti: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* Tarkistaa yhden sivun sisällön sisältömallia vasten. `data` on mitä
|
|
17
|
+
* tahansa, esimerkiksi selaimelta tullut tallennuspyyntö, joten mitään ei
|
|
18
|
+
* oleteta etukäteen.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tarkistaSivu(maarittely: SisallonMaarittely, sivunAvain: string, data: unknown): Tarkistustulos;
|
|
21
|
+
/**
|
|
22
|
+
* Muuntaa tallennetun sivun yhdelle kielelle. Puuttuva käännös korvataan
|
|
23
|
+
* oletuskielellä: vieraskielinen rivi on julkisella sivulla parempi kuin
|
|
24
|
+
* tyhjä kohta, ja editori näyttää puuttuvat käännökset muokkaajalle.
|
|
25
|
+
*/
|
|
26
|
+
export declare function kielista<S extends SivunMaarittely>(maarittely: SisallonMaarittely, sivu: S, data: Record<string, unknown>, kieli: string): Sivu<S>;
|