Skip to content

Commit a886abe

Browse files
authored
Merge pull request #277 from GBSL-Informatik/feature/read-check
feature: add read check
2 parents 17ea12d + 3b8e84b commit a886abe

19 files changed

Lines changed: 838 additions & 4 deletions

File tree

packages/tdev/page-index/README.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ export const Comp = observer(() => {
6262

6363
## Für Plugin-Autoren
6464

65+
:::tip[`@tdev/page-read-check` als Beispiel]
66+
Das Plugin `@tdev/page-read-check` implementiert das `iTaskableDocument`-Interface und wird daher automatisch im Seitenindex erfasst.
67+
Es dient als Beispiel und Referenz, wie ein *Statusdokument* implementiert werden kann, damit es im Seitenindex erfasst wird.
68+
:::
69+
6570
Wenn ein Plugin ein weiteres *Statusdokument* implementiert, das im Seitenindex erfasst werden soll, müssen folgende Schritte durchgeführt werden:
6671
1. In der `siteConfig` die neue Komponente für das `remark`-Plugin registrieren:
6772
```ts title="siteConfig.ts"
@@ -84,7 +89,7 @@ Wenn ein Plugin ein weiteres *Statusdokument* implementiert, das im Seitenindex
8489
export interface TaskableDocumentMapping {
8590
['my_new_taskable_document']: MyNewTaskableData;
8691
}
87-
export interface TypeModelMapping {
92+
export interface TaskableTypeModelMapping {
8893
['my_new_taskable_document']: MyNewTaskableDocument;
8994
}
9095
}
@@ -104,6 +109,7 @@ Wenn ein Plugin ein weiteres *Statusdokument* implementiert, das im Seitenindex
104109
und den neuen Dokumenttyp im `ComponentStore` registrieren:
105110
```ts title="packages/<scope>/<new-plugin>/register.ts"
106111
const register = () => {
112+
rootStore.documentStore.registerFactory('my_new_taskable_document', createModel);
107113
rootStore.componentStore.registerTaskableDocumentType('my_new_taskable_document');
108114
};
109115
```
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React from 'react';
2+
import { observer } from 'mobx-react-lite';
3+
import { useStore } from '@tdev-hooks/useStore';
4+
import { MetaInit, ModelMeta } from '../model/ModelMeta';
5+
import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument';
6+
import SlideButton from '@tdev-components/shared/SlideButton';
7+
import Badge from '@tdev-components/shared/Badge';
8+
import styles from './styles.module.scss';
9+
import clsx from 'clsx';
10+
import { mdiFlashTriangle } from '@mdi/js';
11+
import Icon from '@mdi/react';
12+
import PageReadChecker from '../model';
13+
14+
interface Props extends MetaInit {
15+
id: string;
16+
text?: (unlocked: boolean, doc: PageReadChecker) => string;
17+
disabledReason?: (doc: PageReadChecker) => string;
18+
hideTime?: boolean;
19+
hideWarning?: boolean;
20+
continueAfterUnlock?: boolean;
21+
}
22+
23+
const defaultText = (unlocked: boolean, doc: PageReadChecker) =>
24+
unlocked ? `Gelesen ${doc.fReadTime}` : doc.fReadTime;
25+
const defaultDisabledReason = (doc: PageReadChecker) =>
26+
`Mindestens ${doc.meta.fMinReadTime} lesen, um zu entsperren`;
27+
28+
const PageReadCheck = observer((props: Props) => {
29+
const { text = defaultText, disabledReason = defaultDisabledReason } = props;
30+
const [meta] = React.useState(new ModelMeta(props));
31+
const ref = React.useRef<HTMLDivElement>(null);
32+
33+
const viewStore = useStore('viewStore');
34+
const doc = useFirstMainDocument(props.id, meta);
35+
const [animate, setAnimate] = React.useState(false);
36+
37+
React.useEffect(() => {
38+
if (!viewStore.isPageVisible || !doc) {
39+
return;
40+
}
41+
if (doc.read && !props.continueAfterUnlock) {
42+
return;
43+
}
44+
const id = setInterval(() => {
45+
doc.incrementReadTime(1);
46+
}, 1000);
47+
return () => {
48+
clearInterval(id);
49+
};
50+
}, [doc, doc?.read, viewStore.isPageVisible, props.continueAfterUnlock]);
51+
52+
React.useEffect(() => {
53+
if (ref.current && doc?.scrollTo) {
54+
ref.current.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' });
55+
doc.setScrollTo(false);
56+
setAnimate(true);
57+
}
58+
}, [ref, doc?.scrollTo]);
59+
60+
React.useEffect(() => {
61+
if (animate) {
62+
const timeout = setTimeout(() => {
63+
setAnimate(false);
64+
}, 2000);
65+
return () => {
66+
clearTimeout(timeout);
67+
};
68+
}
69+
}, [animate]);
70+
71+
if (!doc) {
72+
return null;
73+
}
74+
75+
return (
76+
<div className={clsx(styles.pageReadCheck, animate && styles.animate)} ref={ref}>
77+
<SlideButton
78+
text={(unlocked) => text(unlocked, doc)}
79+
onUnlock={() => doc.setReadState(true)}
80+
onReset={() => doc.setReadState(false)}
81+
isUnlocked={doc.read}
82+
disabled={!doc.canUnlock}
83+
sliderWidth={320}
84+
disabledReason={disabledReason(doc)}
85+
/>
86+
<div className={clsx(styles.status)}>
87+
{!doc.canUnlock && (
88+
<Badge title={disabledReason(doc)} className={clsx(styles.minReadTime)}>
89+
{doc.meta.fMinReadTime}
90+
</Badge>
91+
)}
92+
{doc.isDummy && !props.hideWarning && (
93+
<Icon
94+
path={mdiFlashTriangle}
95+
size={0.7}
96+
color="var(--ifm-color-warning)"
97+
title="Wird nicht gespeichert."
98+
/>
99+
)}
100+
</div>
101+
</div>
102+
);
103+
});
104+
105+
export default PageReadCheck;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.pageReadCheck {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
border-radius: var(--ifm-global-radius);
6+
.status {
7+
margin-left: 240px;
8+
min-width: 4em;
9+
display: flex;
10+
gap: 8px;
11+
align-items: center;
12+
justify-content: flex-end;
13+
.minReadTime {
14+
border-top-left-radius: 0;
15+
border-top-right-radius: 0;
16+
}
17+
}
18+
@keyframes flash {
19+
0%,
20+
100% {
21+
opacity: 1;
22+
}
23+
50% {
24+
opacity: 0.7;
25+
}
26+
}
27+
28+
@keyframes shake {
29+
0%,
30+
100% {
31+
transform: translateX(0);
32+
}
33+
10%,
34+
30%,
35+
50%,
36+
70%,
37+
90% {
38+
transform: translateX(-5px);
39+
}
40+
20%,
41+
40%,
42+
60%,
43+
80% {
44+
transform: translateX(5px);
45+
}
46+
}
47+
&.animate {
48+
animation:
49+
flash 1s normal,
50+
shake 1.5s normal;
51+
transform-origin: center;
52+
}
53+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
page_id: 34714568-d5e3-4965-8947-fdcf46da810a
3+
sidebar_custom_props:
4+
taskable_state: 'show'
5+
---
6+
7+
import PageReadCheck from '@tdev/page-read-check/PageReadCheck';
8+
import BrowserWindow from '@tdev-components/BrowserWindow';
9+
import { action } from 'mobx';
10+
11+
# page-read-check
12+
13+
Mit der Komponente `PageReadCheck` kann der Bearbeitungs- und Lesezustand einer Seite als "gelesen" oder "ungelesen" markiert werden.
14+
15+
```tsx
16+
import PageReadCheck from '@tdev/page-read-check/PageReadCheck';
17+
18+
<PageReadCheck id="2cf38080-4b13-4b0f-a589-94e78cce585c" />
19+
```
20+
21+
22+
<BrowserWindow>
23+
<PageReadCheck id="2cf38080-4b13-4b0f-a589-94e78cce585c" />
24+
</BrowserWindow>
25+
26+
## Mit Mindestlesezeit
27+
28+
Über die `minReadTime` kann eine Mindestlesezeit vorgegeben werden, bevor die Seite als "gelesen" markiert werden kann. Der Timer startet und stoppt automatisch, wenn die Seite sichtbar bzw. unsichtbar wird. Der Standardwert beträgt 10 Sekunden.
29+
30+
```tsx
31+
<PageReadCheck minReadTime={15} />
32+
```
33+
34+
<BrowserWindow>
35+
<PageReadCheck minReadTime={15}/>
36+
</BrowserWindow>
37+
38+
## Lesezeit nach Freischaltung fortsetzen
39+
40+
Standardmässig wird die Lesezeit nicht weiter gezählt, wenn die Seite freigeschaltet wurde. Mit der Option `continueAfterUnlock` kann dieses Verhalten geändert werden, sodass die Lesezeit auch nach Freischaltung weiter gezählt wird.
41+
42+
```tsx
43+
<PageReadCheck continueAfterUnlock id="4a84e4a2-dee6-4575-bcd3-9682af03a17d" />
44+
```
45+
46+
<BrowserWindow>
47+
<PageReadCheck continueAfterUnlock id="4a84e4a2-dee6-4575-bcd3-9682af03a17d" />
48+
</BrowserWindow>
49+
50+
## Anpassung der Texte
51+
52+
Die angezeigten Texte und Tooltipps können angepasst werden:
53+
54+
```tsx
55+
<PageReadCheck
56+
text={(unlocked, doc) => {
57+
if (unlocked || doc.canUnlock) {
58+
return `Gelesen ${doc.fReadTime}`;
59+
}
60+
return `Noch ${doc.meta.minReadTime - doc.readTime}s übrig`;
61+
}}
62+
disabledReason={(doc) => `Erst ${doc.fReadTime} bearbeitet, das ist zu wenig!`}
63+
/>
64+
```
65+
66+
<BrowserWindow>
67+
<PageReadCheck
68+
text={(unlocked, doc) => {
69+
if (unlocked || doc.canUnlock) {
70+
return `Gelesen ${doc.fReadTime}`;
71+
}
72+
return `Noch ${doc.meta.minReadTime - doc.readTime}s übrig`;
73+
}}
74+
disabledReason={(doc) => `Erst ${doc.fReadTime} bearbeitet, das ist zu wenig!`}
75+
/>
76+
</BrowserWindow>
77+
78+
## Installation
79+
80+
:::info[`packages/tdev/page-read-check`]
81+
Kopiere des `packages/tdev/page-read-check`-Verzeichnis in das `tdev-website/website/packages`-Verzeichnis oder über `updateTdev.config.yaml` hinzufügen.
82+
:::
83+
84+
Hinzufügen des `page-read-check`-Package zu den `apiDocumentProviders` im `siteConfig.ts`:
85+
86+
```ts title="siteConfig.ts"
87+
const getSiteConfig: SiteConfigProvider = () => {
88+
return {
89+
apiDocumentProviders: [
90+
require.resolve('@tdev/page-read-check/register'),
91+
]
92+
};
93+
};
94+
```
95+
96+
Falls **nicht** die standardoptionen des `PageIndexPluginDefaultOptions` verwendet werden, muss zusätzlich die `remark`-Plugin-Konfiguration angepasst werden, damit die neuen Dokumente im Seitenindex erfasst werden:
97+
98+
```ts title="siteConfig.ts"
99+
import pageIndexPlugin from './packages/tdev/page-index/plugin';
100+
101+
const getSiteConfig: SiteConfigProvider = () => ({
102+
remarkPlugins: [
103+
[
104+
pageIndexPlugin,
105+
{
106+
// ...
107+
components: [
108+
// highlight-start
109+
{
110+
name: 'PageReadCheck',
111+
docTypeExtractor: () => 'page_read_check'
112+
}
113+
// highlight-end
114+
]
115+
}
116+
]
117+
]
118+
});
119+
```
120+
121+
Danach muss erneut installiert werden:
122+
123+
```bash
124+
yarn install
125+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const fSeconds = (time: number) => {
2+
const hours = Math.floor(time / 3600);
3+
const minutes = Math.floor((time % 3600) / 60);
4+
const seconds = time % 60;
5+
if (hours > 0) {
6+
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
7+
}
8+
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
9+
};
10+
11+
export const fSecondsLong = (time: number) => {
12+
const hours = Math.floor(time / 3600);
13+
const minutes = Math.floor((time % 3600) / 60);
14+
const seconds = time % 60;
15+
if (hours > 0) {
16+
return `${hours} Stunden ${minutes.toString().padStart(2, '0')} Minuten ${seconds.toString().padStart(2, '0')} Sekunden`;
17+
}
18+
return `${minutes} Minuten ${seconds.toString().padStart(2, '0')} Sekunden`;
19+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import PageReadCheck from './model';
2+
export interface PageReadCheckData {
3+
readTime: number;
4+
read: boolean;
5+
}
6+
7+
declare module '@tdev-api/document' {
8+
export interface TaskableDocumentMapping {
9+
['page_read_check']: PageReadCheckData;
10+
}
11+
export interface TaskableTypeModelMapping {
12+
['page_read_check']: PageReadCheck;
13+
}
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { TypeDataMapping, Access } from '@tdev-api/document';
2+
import { TypeMeta } from '@tdev-models/DocumentRoot';
3+
import { fSeconds } from '../helpers/time';
4+
5+
export interface MetaInit {
6+
readonly?: boolean;
7+
minReadTime?: number;
8+
}
9+
10+
export class ModelMeta extends TypeMeta<'page_read_check'> {
11+
readonly type = 'page_read_check';
12+
readonly minReadTime: number;
13+
14+
constructor(props: Partial<MetaInit>) {
15+
super('page_read_check', props.readonly ? Access.RO_User : undefined);
16+
this.minReadTime = props.minReadTime || 10;
17+
}
18+
19+
get defaultData(): TypeDataMapping['page_read_check'] {
20+
return {
21+
readTime: 0,
22+
read: false
23+
};
24+
}
25+
26+
get fMinReadTime() {
27+
return fSeconds(this.minReadTime);
28+
}
29+
}

0 commit comments

Comments
 (0)