-
+
{children}
diff --git a/app/markdoc/nodes.ts b/app/markdoc/nodes.tsx
similarity index 95%
rename from app/markdoc/nodes.ts
rename to app/markdoc/nodes.tsx
index 4eb69fb..95afbe8 100644
--- a/app/markdoc/nodes.ts
+++ b/app/markdoc/nodes.tsx
@@ -18,6 +18,7 @@ const nodes = {
this.render,
{
frontmatter: yaml.load(node.attributes.frontmatter),
+ estimatedReadingTime: config?.variables?.estimatedReadingTime,
nodes: node.children,
},
node.transformChildren(config),
diff --git a/app/package.json b/app/package.json
index 105c918..44479a4 100644
--- a/app/package.json
+++ b/app/package.json
@@ -12,6 +12,7 @@
"@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/lexend": "^5.2.5",
"@headlessui/react": "^2.2.0",
+ "@heroicons/react": "^2.2.0",
"@markdoc/markdoc": "^0.5.1",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.16",
@@ -28,6 +29,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight-words": "^0.21.0",
+ "reading-time-estimator": "^1.12.0",
"simple-functional-loader": "^1.2.1",
"telefunc": "^0.1.87",
"unplugin-fonts": "^1.3.1",
diff --git a/app/pages/docs/+Page.tsx b/app/pages/docs/+Page.tsx
index 43ffd68..a675f16 100644
--- a/app/pages/docs/+Page.tsx
+++ b/app/pages/docs/+Page.tsx
@@ -7,10 +7,10 @@ import tags from "@/markdoc/tags";
import React from "react";
export default function Page() {
- const { doc } = useData
();
+ const { doc, estimatedReadingTime } = useData();
const parsedDoc = Markdoc.parse(doc.content);
- const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: {} });
+ const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: { estimatedReadingTime } });
return Markdoc.renderers.react(transformedDoc, React);
}
diff --git a/app/pages/docs/+data.ts b/app/pages/docs/+data.ts
index c0c3613..6cde874 100644
--- a/app/pages/docs/+data.ts
+++ b/app/pages/docs/+data.ts
@@ -1,6 +1,7 @@
import type { PageContext } from "vike/types";
import { docsService } from "@/services/DocsService";
+import { readingTime } from "reading-time-estimator";
import { useConfig } from "vike-react/useConfig";
import buildTitle from "@/pages/buildTitle";
import { render } from "vike/abort";
@@ -18,7 +19,7 @@ export async function data(pageContext: PageContext) {
throw render(404);
}
- console.log({ doc });
+ const readingTimeObject = readingTime(doc.content, 300, "fr");
config({
title: buildTitle(doc.title),
@@ -27,5 +28,5 @@ export async function data(pageContext: PageContext) {
docsService.transform(doc);
- return { doc };
+ return { doc, estimatedReadingTime: readingTimeObject.text };
}
diff --git a/app/pages/star-wars/@id/+Page.tsx b/app/pages/star-wars/@id/+Page.tsx
deleted file mode 100644
index 6e50b0e..0000000
--- a/app/pages/star-wars/@id/+Page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-import { useData } from "vike-react/useData";
-import type { Data } from "./+data.js";
-
-export default function Page() {
- const movie = useData();
- return (
- <>
- {movie.title}
- Release Date: {movie.release_date}
-
- Director: {movie.director}
-
- Producer: {movie.producer}
- >
- );
-}
diff --git a/app/pages/star-wars/@id/+data.ts b/app/pages/star-wars/@id/+data.ts
deleted file mode 100644
index 3b5a342..0000000
--- a/app/pages/star-wars/@id/+data.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// https://vike.dev/data
-
-import type { PageContextServer } from "vike/types";
-import type { MovieDetails } from "../types.js";
-import { useConfig } from "vike-react/useConfig";
-
-export type Data = Awaited>;
-
-export const data = async (pageContext: PageContextServer) => {
- // https://vike.dev/useConfig
- const config = useConfig();
-
- const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
- let movie = (await response.json()) as MovieDetails;
-
- config({
- // Set
- title: movie.title,
- });
-
- // We remove data we don't need because the data is passed to
- // the client; we should minimize what is sent over the network.
- movie = minimize(movie);
-
- return movie;
-};
-
-function minimize(movie: MovieDetails): MovieDetails {
- const { id, title, release_date, director, producer } = movie;
- const minimizedMovie = { id, title, release_date, director, producer };
- return minimizedMovie;
-}
diff --git a/app/pages/star-wars/index/+Page.tsx b/app/pages/star-wars/index/+Page.tsx
deleted file mode 100644
index 3c24a4e..0000000
--- a/app/pages/star-wars/index/+Page.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { useData } from "vike-react/useData";
-import type { Data } from "./+data.js";
-
-export default function Page() {
- const movies = useData();
- return (
- <>
- Star Wars Movies
-
- {movies.map(({ id, title, release_date }) => (
- -
- {title} ({release_date})
-
- ))}
-
-
- Source: brillout.github.io/star-wars.
-
- >
- );
-}
diff --git a/app/pages/star-wars/index/+data.ts b/app/pages/star-wars/index/+data.ts
deleted file mode 100644
index 73834ea..0000000
--- a/app/pages/star-wars/index/+data.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// https://vike.dev/data
-
-import type { Movie, MovieDetails } from "../types.js";
-import { useConfig } from "vike-react/useConfig";
-
-export type Data = Awaited>;
-
-export const data = async () => {
- // https://vike.dev/useConfig
- const config = useConfig();
-
- const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
- const moviesData = (await response.json()) as MovieDetails[];
-
- config({
- // Set
- title: `${moviesData.length} Star Wars Movies`,
- });
-
- // We remove data we don't need because the data is passed to the client; we should
- // minimize what is sent over the network.
- const movies = minimize(moviesData);
-
- return movies;
-};
-
-function minimize(movies: MovieDetails[]): Movie[] {
- return movies.map((movie) => {
- const { title, release_date, id } = movie;
- return { title, release_date, id };
- });
-}
diff --git a/app/pages/star-wars/types.ts b/app/pages/star-wars/types.ts
deleted file mode 100644
index ffccdf5..0000000
--- a/app/pages/star-wars/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type Movie = {
- id: string;
- title: string;
- release_date: string;
-};
-
-export type MovieDetails = Movie & {
- director: string;
- producer: string;
-};
diff --git a/app/pages/todo/+Page.tsx b/app/pages/todo/+Page.tsx
deleted file mode 100644
index 674b784..0000000
--- a/app/pages/todo/+Page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { Data } from "./+data";
-import React from "react";
-import { useData } from "vike-react/useData";
-import { TodoList } from "./TodoList.js";
-
-export default function Page() {
- const data = useData();
- return (
- <>
- To-do List
-
- >
- );
-}
diff --git a/app/pages/todo/+config.ts b/app/pages/todo/+config.ts
deleted file mode 100644
index a668c0a..0000000
--- a/app/pages/todo/+config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const config = {
- prerender: false,
-};
diff --git a/app/pages/todo/+data.ts b/app/pages/todo/+data.ts
deleted file mode 100644
index 60954bf..0000000
--- a/app/pages/todo/+data.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// https://vike.dev/data
-import { todos } from "../../database/todoItems";
-import type { PageContextServer } from "vike/types";
-
-export type Data = {
- todo: { text: string }[];
-};
-
-export default async function data(_pageContext: PageContextServer): Promise {
- return { todo: todos };
-}
diff --git a/app/pages/todo/TodoList.telefunc.ts b/app/pages/todo/TodoList.telefunc.ts
deleted file mode 100644
index abb5d74..0000000
--- a/app/pages/todo/TodoList.telefunc.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// We use Telefunc (https://telefunc.com) for data mutations. Being able to use Telefunc for fetching initial data is work-in-progress (https://vike.dev/data-fetching#tools).
-
-import { todos } from "../../database/todoItems";
-
-export async function onNewTodo({ text }: { text: string }) {
- todos.push({ text });
-}
diff --git a/app/pages/todo/TodoList.tsx b/app/pages/todo/TodoList.tsx
deleted file mode 100644
index 0b3483c..0000000
--- a/app/pages/todo/TodoList.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { onNewTodo } from "./TodoList.telefunc";
-import React, { useState } from "react";
-
-export function TodoList({ initialTodoItems }: { initialTodoItems: { text: string }[] }) {
- const [todoItems, setTodoItems] = useState(initialTodoItems);
- const [newTodo, setNewTodo] = useState("");
- return (
- <>
-
- {todoItems.map((todoItem, index) => (
- // biome-ignore lint:
- - {todoItem.text}
- ))}
-
-
-
-
- >
- );
-}
diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml
index 52d3206..e86f18a 100644
--- a/app/pnpm-lock.yaml
+++ b/app/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
'@headlessui/react':
specifier: ^2.2.0
version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@heroicons/react':
+ specifier: ^2.2.0
+ version: 2.2.0(react@19.0.0)
'@markdoc/markdoc':
specifier: ^0.5.1
version: 0.5.1(@types/react@19.0.10)(react@19.0.0)
@@ -74,6 +77,9 @@ importers:
react-highlight-words:
specifier: ^0.21.0
version: 0.21.0(react@19.0.0)
+ reading-time-estimator:
+ specifier: ^1.12.0
+ version: 1.12.0
simple-functional-loader:
specifier: ^1.2.1
version: 1.2.1
@@ -719,6 +725,11 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
+ '@heroicons/react@2.2.0':
+ resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==}
+ peerDependencies:
+ react: '>= 16 || ^19.0.0-rc'
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -1401,6 +1412,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1425,6 +1440,19 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1449,6 +1477,10 @@ packages:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
es-abstract@1.23.9:
resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==}
engines: {node: '>= 0.4'}
@@ -1772,6 +1804,9 @@ packages:
highlight-words-core@1.2.3:
resolution: {integrity: sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==}
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -1867,6 +1902,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -2180,6 +2219,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-srcset@1.0.2:
+ resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
+
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@@ -2310,6 +2352,9 @@ packages:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
+ reading-time-estimator@1.12.0:
+ resolution: {integrity: sha512-A17wehSIG6bxen84mf5m+MP4bDDZytcfMOfJWDp1DT8StGRgljtD58zC6E+fSnQZvU5mfsjiJZ7BLhr9kHYoQQ==}
+
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@@ -2385,6 +2430,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ sanitize-html@2.15.0:
+ resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==}
+
scheduler@0.25.0:
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
@@ -3298,6 +3346,10 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
+ '@heroicons/react@2.2.0(react@19.0.0)':
+ dependencies:
+ react: 19.0.0
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -4007,6 +4059,8 @@ snapshots:
deep-is@0.1.4: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -4029,6 +4083,24 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4050,6 +4122,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.1
+ entities@4.5.0: {}
+
es-abstract@1.23.9:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -4535,6 +4609,13 @@ snapshots:
highlight-words-core@1.2.3: {}
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 4.5.0
+
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -4634,6 +4715,8 @@ snapshots:
is-number@7.0.0: {}
+ is-plain-object@5.0.0: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -4918,6 +5001,8 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-srcset@1.0.2: {}
+
path-browserify@1.0.1: {}
path-exists@4.0.0: {}
@@ -5043,6 +5128,10 @@ snapshots:
react@19.0.0: {}
+ reading-time-estimator@1.12.0:
+ dependencies:
+ sanitize-html: 2.15.0
+
real-require@0.2.0: {}
reflect.getprototypeof@1.0.10:
@@ -5143,6 +5232,15 @@ snapshots:
safer-buffer@2.1.2: {}
+ sanitize-html@2.15.0:
+ dependencies:
+ deepmerge: 4.3.1
+ escape-string-regexp: 4.0.0
+ htmlparser2: 8.0.2
+ is-plain-object: 5.0.0
+ parse-srcset: 1.0.2
+ postcss: 8.5.3
+
scheduler@0.25.0: {}
search-insights@2.17.3: {}