Compare commits

...

30 Commits

Author SHA1 Message Date
github-actions[bot]
6aeadd71de Version Packages (#942)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-09 00:16:30 +02:00
olayway
affca05058 [remark-callouts/package.json][xs]: fix export path 2023-06-09 00:12:03 +02:00
github-actions[bot]
f54d238795 Version Packages (#941)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-08 23:57:47 +02:00
olayway
e82e2ae021 [packages][xs]: replace prepublish with prepare 2023-06-08 23:53:42 +02:00
github-actions[bot]
c3246ee7f8 Version Packages (#940)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-08 23:45:12 +02:00
olayway
40d80d2282 [packages/*][s]: add prepublish script to build first 2023-06-08 23:42:17 +02:00
olayway
e0e720338f [components/package.json][xs]: add prepublish script 2023-06-08 23:33:34 +02:00
olayway
4f8b1b1e96 [ckan/package.json][xs]: add prepublish script 2023-06-08 23:32:29 +02:00
olayway
362afcc133 [package.json][xs]: rm faulty prerelease script 2023-06-08 23:29:17 +02:00
João Demenech
c165b3cc44 merge: new data story about ESIF funds
A new data story about ESIF funds
2023-06-08 17:22:27 -03:00
João Demenech
261a2a081e Fix minor typos 2023-06-08 17:21:43 -03:00
Anuar Ustayev (aka Anu)
d27857f490 Added conclusion and final tweaks. 2023-06-08 20:47:59 +07:00
Anuar Ustayev (aka Anu)
b3ba263bd8 Switch from raw csv to remote file in r2. 2023-06-08 20:35:33 +07:00
Anuar Ustayev (aka Anu)
cb774d0ad0 Added bar chart for comparison between countries. 2023-06-08 20:28:05 +07:00
Anuar Ustayev (aka Anu)
b48f71ecef Add updated data with currency consideration. 2023-06-08 20:07:03 +07:00
Anuar Ustayev (aka Anu)
07b3235647 Create where-the-european-structural-and-investment-funds-go.mdx 2023-06-08 18:19:57 +07:00
Luccas Mateus de Medeiros Gomes
d0c2ee1e71 [examples/openspending][xs] - add contributing tab 2023-06-07 15:31:40 -03:00
João Demenech
bc180189cb Update OS sample data story 2023-06-07 11:28:31 -03:00
github-actions[bot]
39c862627d Version Packages (#937)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-07 15:22:12 +02:00
olayway
b7158a5be6 add changeset file 2023-06-07 15:17:46 +02:00
olayway
ee87c4f623 [remark-embed][s]: don't include README in dist
- it will already be included by npm anyway
2023-06-07 15:06:08 +02:00
olayway
4141af0e82 [remark-embed][s]: specify folder to publish 2023-06-07 15:05:29 +02:00
olayway
7d36d22671 [remark-wiki-link][xs]: don't include README in /dist
- it is included automatically by npm, in the parent folder
2023-06-07 15:04:46 +02:00
olayway
eab2d65113 [remark-wiki-link][s]: specify folder to publish 2023-06-07 15:04:46 +02:00
olayway
51d0a7692e [remark-callouts][xs]: don't include README in /dist
- it is included automatically by npm, in the parent folder
2023-06-07 15:04:46 +02:00
olayway
cdd90ac384 [remak-callouts][s]: specify folder to publish in package.json 2023-06-07 15:04:46 +02:00
olayway
dcf6400304 [core][xs]: don't include README in /dist
- it is included automatically by npm, in the parent folder
2023-06-07 15:04:46 +02:00
olayway
247b2412d6 [core][xs]: specify folder to publish 2023-06-07 15:04:41 +02:00
olayway
1ad9b85e02 [package.json][s]: add building to prerelease 2023-06-07 13:56:32 +02:00
Ola Rubaj
af134cac8b Integrate flowershow packages (#923)
* [packages][m]: mv @flowershow/core package here

* [packages/core][xs]: rename to @portaljs/core

* [package.json][xs]: setup npm workspaces

* [packages/core][xs]:replace deprecated rollup executor

* [core/package.json][s]: fix mermaid versions

* [core/tsconfig][xs]: rm extends

* [core/jest.config][xs]: rm coverageDirectory

* [core/package.json][xs]: install core-js

* [packages.json][s]:use same version for all nrwl packages

* [core/.eslintrc][xs]: adjust ignorePatterns

* [core/project.json][xs]: rm publish targets

* [packages][m]: mv @flowershow/remark-wiki-link here

* [packages][m]: mv @flowershow/remark-wiki-link here

* [packages][m]: mv @flowershow/remark-embed here

* [remark-callouts/project.json][xs]: adjst test pattern

* [package.json][s]: install missing deps

* [remark-callouts][xs]: adjst fields in package.json

* [remark-callouts][s]: rm pubish targets and adjst build executor

* [remark-embed/jest.config][xs]: rm unknown option coverageDirectory

* [remark-embed][xs]: rm publish targets

* [remark-embed][s]: rename to @portaljs/remark-embed

* [remark-wiki-link/eslintrc][xs]:adjst ignorePatterns

* [package.json][xs]: install missing deps

* [remark-wiki-link/test][xs]:specify format

- also temporarily force any type on htmlExtension

* [remark-wiki-link/README][xs]: replace @flowershow with @portaljs

* [remark-wiki-link][xs]:rm old changelog

* [remark-wiki-link][xs]: adjst package.json

* [remark-wiki-link/project.json][xs]: rm publish targets

* [core][s]: rm old changelog

* [core/README][xs]:correct scope name

* [remark-callouts/README][xs]: add @portaljs to pckg name

* [remark-embed/README][xs]: add @portaljs to pckg name

* [package-lock.json][xs]: refresh after rebasing on main
2023-06-07 07:21:00 -03:00
151 changed files with 10580 additions and 2321 deletions

View File

@@ -53,6 +53,10 @@ export function Header() {
},
],
},
{
title: 'Contributing',
href: '/contributing',
},
{
title: 'Resources',
href: '/resources',

View File

@@ -0,0 +1,36 @@
# How to contribute
OpenSpending is a project that aims to make public financial data more accessible, understandable, and usable. It is powered by PortalJS, a framework for building data portals that are fast, secure, and easy to customize.
If you have any questions, the best place to get answers is to reach to us on [Discord](https://discord.gg/xJrxCbkP)
We welcome contributions from anyone who is interested in improving OpenSpending and making it more useful for the public. There are many ways you can contribute to the project, such as:
- Submitting datasets for evaluation and inclusion in OpenSpending
- Reporting bugs or issues with the website or the data
- Suggesting new features or enhancements
- Providing feedback or ideas
- Helping other users or answering questions
## Submitting a contribution
The main platform for communication and collaboration for OpenSpending is Github, if you want to interact with us you can o so by submitting an issue.
If you want to submit a dataset for evaluation and inclusion in OpenSpending, you will need to create an issue on Github using this template:
```markdown
Title: [Dataset Submission] Name of the dataset (This will be the name of the repo where your datasets will be stored)
Readme: A description of your data to include in the new repo that will be created
Datapackage: All our datasets require a datapackage following the [frictionless spec](https://specs.frictionlessdata.io/) more specific the [fiscal version](https://specs.frictionlessdata.io/fiscal-data-package/)
```
Please make sure that the dataset meets the following criteria before submitting it:
- The dataset contains public financial data (e.g., budgets, expenditures, revenues, contracts)
- The dataset is open and free to use (e.g., no restrictions on access or reuse)
- The dataset is structured and machine-readable (e.g., no scanned images or PDFs)
- The dataset has a datapackage containing metadata and documentation (e.g., descriptions, definitions, schemas)
If you have any questions or doubts about submitting a dataset, please contact us on Github or join our [Discord server](https://discord.gg/xJrxCbkP), where you can chat with other contributors and get support from the OpenSpending team.
We appreciate your interest and involvement in OpenSpending, and we look forward to working with you on making public financial data more open and transparent. Thank you for your contribution! 😊

View File

@@ -3,7 +3,7 @@ title: Sample Data Story
date: 06/06/2023
---
This is a sample data story, you can add charts
The below chart clearly shows the steep upward trend of oil prices these are largely due to social-political factors and civil unrest:
<LineChart
data="https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv"
@@ -12,23 +12,26 @@ This is a sample data story, you can add charts
yAxis="Price"
/>
Or you can add previews
But to really understand the different global factors that influence the price of a commodity like oil the long term trends as shown above is not adequate.
When considering the yearly fluctuations it is often beneficial to filter certain time periods.
One can use the below preview to filter year periods. Alternatively, if you want to consider the points in time when the price of oil was the highest the price range can also be filtered.
<FlatUiTable url="https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv" />
And you can of course add markdown
## Subtitles
- Lists
- Lists
You can also add mermaid charts
Careful data analysis has identified instability albeit economic or political to be the major driver of oil price flatuations
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
graph LR;
A[Socio-Political Instability]-->B[War & Civil Unrest];
A-->C[Economic Factors];
B-->D[Price Increase];
C-->D;
```
___
**To find the raw data used in this article visit https://github.com/datasets/oil-prices/**

View File

@@ -0,0 +1,158 @@
---
title: Where the European Structural and Investment Funds go?
date: 2023-06-08
authors: ['Anuar Ustayev']
---
European Structural and Investment Funds (ESIF) is a financial instrument used by the European Union (EU) to support regional development and economic cohesion among its member states. The ESIF combines several funds to provide financial assistance to regions and cities in the EU with the aim of reducing economic disparities and promoting sustainable growth.
In this data story, our objective is to determine which country benefits the most from the ESIF funds and identify the region within the EU that receives the highest amount of funding.
To begin our analysis, we are using data on the allocation of ESIF funds across EU member states and their respective regions. These datasets are provided by previous work at OpenSpending project. See available datasets:
- Full dataset: https://www.openspending.org/@os-data/complete-european-esif-funds-beneficiaries-2007-2020
- By country, e.g., this is for Austria: https://www.openspending.org/@os-data/complete-european-esif-funds-beneficiaries-2007-2020-filtered-by-at
The data provide insights into the financial assistance provided by the European Union to support regional development and economic cohesion:
<FlatUiTable url="https://storage.openspending.org/complete-european-esif-funds-beneficiaries-2007-2020/eu-esif-funds-beneficiaries-2007-2020-full.csv" />
After processing the available datasets we have derived an aggregated data resource that groups data by country which enables us to understand where the most funding went between 2007 and 2020. Notice that maximum amounts in each column are highlighted in the table but they might be in local currencies so check the 'currency' field:
<FlatUiTable url="https://storage.openspending.org/eu-esif-funds-beneficiaries-2007-2020-full-aggregated-by-country.csv" />
Quick observations:
- The Czech Republic has the EU cofinancing amount of CZK 2,731,165,279,234 which is approximately EUR 115 billions.
- Poland has the EU cofinancing amount of PLN 302,958,765,476 which is approximately EUR 67 billions.
- Other countries that use local currencies do not have significant amounts when comparing to above two nations.
So let's convert all currencies to ~EUR but only include 'eu_cofinancing_amount' values as we will use it for comparison:
<FlatUiTable rawCsv="beneficiary_country_code,eu_cofinancing_amount
AT,0.0
BE,858388537.42
BG,0.0
CY,0.0
CZ,115591108113
DE,912803359.21
DK,492017672
EE,5419641772.0
EL,0.0
ES,0.0
FI,2858282488.0
FR,15027864492.94
HR,0.0
HU,0.0
IE,0.0
IT,34860253672.98
LT,6750675528.74
LU,30852529.89
LV,0.0
MT,0.0
NL,0.0
PL,67537688711
PT,23800568868.02
RO,19970864938.12
SE,0.0
SI,4576151396.24
SK,12295960193.92
UK,12829255012.10" />
With this data table we can easily see top countries by funding. Note that you can use the table component above to sort values by values in 'eu_cofinancing_amount' column which helps you to quickly see top countries. Let's build a quick visualization to make it even more obvious:
<Vega
data={{
table: [
{
x: "LU",
y: 30852529.89
},
{
x: "DK",
y: 492017672
},
{
x: "BE",
y: 858388537.42
},
{
x: "DE",
y: 912803359.21
},
{
x: "FI",
y: 2858282488
},
{
x: "SI",
y: 4576151396.24
},
{
x: "EE",
y: 5419641772
},
{
x: "LT",
y: 6750675528.74
},
{
x: "SK",
y: 12295960193.92
},
{
x: "UK",
y: 12829255012.1
},
{
x: "FR",
y: 15027864492.94
},
{
x: "RO",
y: 19970864938.12
},
{
x: "PT",
y: 23800568868.02
},
{
x: "IT",
y: 34860253672.98
},
{
x: "PL",
y: 67537688711
},
{
x: "CZ",
y: 115591108113
}
]
}}
spec={{
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
data: {
name: 'table'
},
encoding: {
x: {
field: 'x',
type: 'ordinal'
},
y: {
field: 'y',
type: 'quantitative'
}
},
mark: 'bar'
}}
/>
Based on the bar chart above we can conclude that the following 3 countries have received the most amounts from ESIF fund:
1. Czech Republic - EUR ~116b.
2. Poland - EUR ~68b.
3. Italy - EUR ~35b.
_This data story was created by using Datopian's PortalJS framework. You can learn more about the framework by visiting https://portaljs.org/_

6748
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"license": "MIT",
"scripts": {
"changeset": "changeset",
"prerelease": "nx affected --targets=lint,test",
"prerelease": "nx run-many --targets=lint,test,build --projects=tag:test",
"release": "changeset publish"
},
"private": true,
@@ -14,11 +14,11 @@
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.1",
"@nrwl/cypress": "15.9.2",
"@nrwl/eslint-plugin-nx": "^16.0.2",
"@nrwl/eslint-plugin-nx": "15.9.2",
"@nrwl/jest": "15.9.2",
"@nrwl/js": "15.9.2",
"@nrwl/linter": "15.9.2",
"@nrwl/next": "^15.9.2",
"@nrwl/next": "15.9.2",
"@nrwl/react": "15.9.2",
"@nrwl/rollup": "15.9.2",
"@nrwl/workspace": "15.9.2",
@@ -28,13 +28,16 @@
"@swc/helpers": "~0.5.0",
"@swc/jest": "0.2.20",
"@testing-library/react": "14.0.0",
"@types/chai": "^4.3.5",
"@types/jest": "^29.4.0",
"@types/mocha": "^10.0.1",
"@types/node": "18.14.2",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"babel-jest": "^29.4.1",
"chai": "^4.3.7",
"cypress": "^12.2.0",
"eslint": "~8.15.0",
"eslint-config-next": "13.1.1",
@@ -44,14 +47,21 @@
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"htmlparser2": "^9.0.0",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"micromark": "^3.2.0",
"mocha": "^10.2.0",
"nx": "15.9.2",
"prettier": "^2.6.2",
"react-test-renderer": "18.2.0",
"rehype-stringify": "^9.0.3",
"remark": "^14.0.3",
"swc-loader": "0.1.15",
"ts-jest": "^29.0.5",
"ts-node": "10.9.1",
"typescript": "~4.9.5"
"typescript": "~4.9.5",
"unist-util-select": "^4.0.3",
"unist-util-visit": "^4.1.2"
}
}

View File

@@ -0,0 +1,7 @@
# @portaljs/ckan
## 0.0.3
### Patch Changes
- [`e82e2ae`](https://github.com/datopian/portaljs/commit/e82e2ae0211ea3e4701703d353b44cf1001434ef) Thanks [@olayway](https://github.com/olayway)! - Fix: replace deprecated `prepublish` script with `prepare`

View File

@@ -1,6 +1,6 @@
{
"name": "@portaljs/ckan",
"version": "0.0.2",
"version": "0.0.3",
"type": "module",
"description": "https://portaljs.org",
"keywords": [
@@ -13,7 +13,8 @@
"scripts": {
"build": "tsc && vite build && npm run build-tailwind",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify"
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify",
"prepare": "npm run build"
},
"peerDependencies": {
"react": "^18.2.0",

View File

@@ -1,5 +1,17 @@
# @portaljs/components
## 0.1.11
### Patch Changes
- [`e82e2ae`](https://github.com/datopian/portaljs/commit/e82e2ae0211ea3e4701703d353b44cf1001434ef) Thanks [@olayway](https://github.com/olayway)! - Fix: replace deprecated `prepublish` script with `prepare`
## 0.1.10
### Patch Changes
- [`40d80d2`](https://github.com/datopian/portaljs/commit/40d80d2282bf8464c1aafb393975065078ad9ea3) Thanks [@olayway](https://github.com/olayway)! - Fix: missing files in the published package.
## 0.1.9
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@portaljs/components",
"version": "0.1.9",
"version": "0.1.11",
"type": "module",
"description": "https://portaljs.org",
"keywords": [
@@ -16,7 +16,8 @@
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify"
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify",
"prepare": "npm run build"
},
"peerDependencies": {
"react": "^18.2.0",

12
packages/core/.babelrc Normal file
View File

@@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "dist/**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,19 @@
# @portaljs/core
## 1.0.3
### Patch Changes
- [`e82e2ae`](https://github.com/datopian/portaljs/commit/e82e2ae0211ea3e4701703d353b44cf1001434ef) Thanks [@olayway](https://github.com/olayway)! - Fix: replace deprecated `prepublish` script with `prepare`
## 1.0.2
### Patch Changes
- [`40d80d2`](https://github.com/datopian/portaljs/commit/40d80d2282bf8464c1aafb393975065078ad9ea3) Thanks [@olayway](https://github.com/olayway)! - Fix: missing files in the published package.
## 1.0.1
### Patch Changes
- [`b7158a5`](https://github.com/datopian/portaljs/commit/b7158a5be668018d9b947f9c9d63fa30fa91d18b) Thanks [@olayway](https://github.com/olayway)! - Fix what's getting published to npm.

3
packages/core/README.md Normal file
View File

@@ -0,0 +1,3 @@
# @portaljs/core
Core Portal.JS package containing components, styles, and utils.

View File

@@ -0,0 +1,9 @@
/* eslint-disable */
export default {
displayName: "core",
preset: "../../jest.preset.js",
transform: {
"^.+\\.[tj]sx?$": "babel-jest",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx"]
};

View File

@@ -0,0 +1,48 @@
{
"name": "@portaljs/core",
"version": "1.0.3",
"description": "Core Portal.JS components, configs and utils.",
"repository": {
"type": "git",
"url": "git+https://github.com/datopian/portaljs.git",
"directory": "packages/core"
},
"author": "Rufus Pollock",
"license": "MIT",
"bugs": {
"url": "https://github.com/datopian/portaljs/issues"
},
"homepage": "https://github.com/datopian/portaljs#readme",
"publishConfig": {
"access": "public"
},
"scripts": {
"prepare": "nx build core"
},
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.js",
"dependencies": {
"@docsearch/react": "^3.3.3",
"@floating-ui/react-dom": "^1.3.0",
"@floating-ui/react-dom-interactions": "^0.13.3",
"@giscus/react": "^2.2.6",
"@headlessui/react": "^1.7.12",
"clsx": "^1.2.1",
"core-js": "^3.30.2",
"disqus-react": "^1.1.5",
"framer-motion": "^10.0.1",
"kbar": "0.1.0-beta.40",
"mdx-mermaid": "^1.3.2",
"mermaid": "^10.2.2",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"next": "^13.2.1",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@@ -0,0 +1,39 @@
{
"name": "core",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/core/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nrwl/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "packages/core/dist",
"tsConfig": "packages/core/tsconfig.lib.json",
"project": "packages/core/package.json",
"entryFile": "packages/core/src/index.ts",
"format": ["esm"],
"generateExportsField": true,
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": []
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/core/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "packages/core/jest.config.ts",
"passWithNoTests": true
}
}
}
}

View File

@@ -0,0 +1,31 @@
export const defaultConfig = {
title: "Flowershow",
description: "",
showEditLink: false,
showToc: true,
showSidebar: false,
showLinkPreviews: true,
author: "",
authorLogo: "",
domain: "",
// Google analytics key e.g. G-XXXX
analytics: "",
// content source directory for markdown files
// DO NOT CHANGE THIS VALUE
// if you have your notes in another (external) directory,
// /content dir should be a symlink to that directory
content: "content",
avatarPlaceholder: "/_flowershow/avatarplaceholder.png",
contentExclude: [],
contentInclude: [],
blogDir: "blog",
peopleDir: "people",
// Theme
theme: {
default: "dark",
toggleIcon: "/_flowershow/theme-button.svg",
},
navLinks: [
// { href: '/about', name: 'About' },
],
};

View File

@@ -0,0 +1 @@
export { defaultConfig } from "./default";

View File

@@ -0,0 +1,3 @@
export * from "./ui";
export * from "./utils";
export * from "./config";

7
packages/core/src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export {};
declare global {
interface Window {
gtag: any; // TODO
}
}

View File

@@ -0,0 +1,24 @@
// TODO
type Props = any;
export const Avatar: React.FC<Props> = ({ name, img, href }) => {
const Component = href ? "a" : "div";
return (
<Component href={href} className="group block flex-shrink-0 mt-2">
<div className="flex items-center space-x-2">
<div>
<img
className="inline-block h-9 w-9 rounded-full"
src={img}
alt={name}
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-primary dark:text-primary-dark">
{name}
</p>
</div>
</div>
</Component>
);
};

View File

@@ -0,0 +1 @@
export { Avatar } from "./Avatar";

View File

@@ -0,0 +1,15 @@
import Link from "next/link.js";
import { forwardRef } from "react";
const BaseLink = forwardRef((props: any, ref) => {
const { href, children, ...rest } = props;
return (
<Link href={href} ref={ref} {...rest}>
{children}
</Link>
);
});
BaseLink.displayName = "BaseLink";
export { BaseLink };

View File

@@ -0,0 +1,57 @@
import Link from "next/link.js";
import { Tooltip } from "../Tooltip";
import TwitterEmbed from "./TwitterEmbed";
// TODO it's a mess, move twitter embeds support to remark-embed
const TWITTER_REGEX =
/^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(es)?\/(\d+)/;
interface Props {
href: string;
data: any;
usehook: any;
preview: boolean;
children: React.ReactNode;
className?: string;
[x: string]: unknown;
}
export const CustomLink: React.FC<Props> = ({
data,
usehook,
preview,
...props
}) => {
const { href } = props;
const isInternalLink = !href.startsWith("http");
// eslint-disable-next-line no-useless-escape
const isHeadingLink = href.startsWith("#");
const isTwitterLink = TWITTER_REGEX.test(href);
// Use next link for pages within app and <a> for external links.
// https://nextjs.org/learn/basics/navigate-between-pages/client-side
if (isInternalLink) {
if (preview && !isHeadingLink) {
return (
<Tooltip
{...props}
data={data} // TODO again, why do we pass all documents here?!
usehook={usehook}
render={(tooltipTriggerProps) => <Link {...tooltipTriggerProps} />}
/>
);
} else {
return <Link {...props} />;
}
}
if (isTwitterLink) {
return <TwitterEmbed url={href} {...props} />;
}
return (
<a target="_blank" rel="noopener noreferrer" {...props}>
{props.children}
</a>
);
};

View File

@@ -0,0 +1,44 @@
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
interface Props {
defaultTheme: "dark" | "light";
toggleIcon: string;
}
export const ThemeSelector: React.FC<Props> = ({
defaultTheme,
toggleIcon,
}) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => setMounted(true), []);
/** Avoid Hydration Mismatch
* https://github.com/pacocoursey/next-themes#avoid-hydration-mismatch
*/
if (!mounted) return null;
// TODO why?
if (!defaultTheme) return null;
return (
<button
type="button"
className={`
min-w-fit transition duration-500
${theme === "dark" ? "grayscale opacity-70" : ""}
`}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<img
src={toggleIcon}
alt="toggle theme"
width={24}
height={24}
className="max-w-24 max-h-24"
/>
</button>
);
};

View File

@@ -0,0 +1,105 @@
// TODO dark and light theme
import { useEffect, useState, useRef, RefObject } from "react";
const twitterWidgetJs = "https://platform.twitter.com/widgets.js";
enum TweetState {
LOADING,
LOADED,
FAILED,
}
interface TweetConfig {
theme: string;
}
declare global {
interface Window {
twttr: {
widgets: {
createTweet: (
id: string,
ref: RefObject<HTMLDivElement>,
options: TweetConfig
) => Promise<any>; // TODO type
load: (ref: RefObject<HTMLDivElement>) => void;
};
};
}
}
export default function TwitterEmbed({ url, ...props }) {
const ref = useRef<HTMLDivElement | null>(null);
const [tweetState, setTweetState] = useState<TweetState>(TweetState.LOADING);
const tweetId = url.split("status/").pop();
useEffect(() => {
const renderTweet = () => {
window.twttr.widgets
.createTweet(tweetId, ref.current as any, {
theme: "dark",
})
.then((el) => {
if (el) {
setTweetState(TweetState.LOADED);
} else {
setTweetState(TweetState.FAILED);
}
});
return window.twttr.widgets.load(ref.current as any);
};
if (!window.twttr) {
const script = document.createElement("script");
script.src = twitterWidgetJs;
script.async = true;
script.onload = () => renderTweet();
document.head.appendChild(script);
} else {
renderTweet();
}
}, [tweetId]);
return (
<>
{tweetState === TweetState.LOADING && (
<div className="relative my-4 w-full sm:max-w-xl bg-neutral-900 drop-shadow-md rounded-lg">
<div className="absolute flex flex-col flex-wrap break-all items-center justify-center bg-slate-700/60 w-full h-full px-4 py-2 rounded-lg top-0 left-0 z-10">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-6 absolute right-4 top-4"
>
<title>Twitter</title>
<path
fill="#1DA1F2"
d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
/>
</svg>
<div className="text-gray-300 font-bold my-2 italic">
{"Loading tweet..."}
</div>
</div>
<div className="p-3 space-y-4 animate-pulse">
<div className="flex items-center">
<div className="mr-2 h-10 w-10 rounded-full bg-slate-700" />
<div className="w-1/3 h-4 bg-slate-700"></div>
</div>
<div className="space-y-2">
<div className="w-2/3 h-3 bg-slate-700"></div>
<div className="w-2/3 h-3 bg-slate-700"></div>
</div>
<div className="flex space-x-4">
<div className="w-1/4 h-3 bg-slate-700"></div>
<div className="w-1/4 h-3 bg-slate-700"></div>
<div className="w-1/4 h-3 bg-slate-700"></div>
</div>
</div>
</div>
)}
<div className="twitter-tweet" ref={ref} />
</>
);
}

View File

@@ -0,0 +1,3 @@
export { BaseLink } from "./BaseLink";
export { ThemeSelector } from "./ThemeSelector";
export { CustomLink } from "./CustomLink";

View File

@@ -0,0 +1,21 @@
export function Avatar({ name, img, href }) {
const Component = href ? "a" : "div";
return (
<Component href={href} className="group block flex-shrink-0 mt-2">
<div className="flex items-center space-x-2">
<div>
<img
className="inline-block h-9 w-9 rounded-full"
src={img}
alt={name}
/>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-primary dark:text-primary-dark">
{name}
</p>
</div>
</div>
</Component>
);
}

View File

@@ -0,0 +1,40 @@
import { Card } from "../Card";
import { formatDate } from "../../utils/formatDate";
import { Blog } from "../types";
interface Props {
blog: Blog;
}
export const BlogItem: React.FC<Props> = ({ blog }) => {
return (
<article className="blogitem md:grid md:grid-cols-4 md:items-baseline">
<Card className="blogitem-card md:col-span-3">
<Card.Title className="blogitem-title" href={`${blog.urlPath}`}>
{blog.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={blog.date}
className="blogitem-date md:hidden"
decorate
>
{formatDate(blog.date)}
</Card.Eyebrow>
{blog.description && (
<Card.Description className="blogitem-descr">
{blog.description}
</Card.Description>
)}
<Card.Cta className="blogitem-cta">Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={blog.date}
className="blogitem-date mt-1 hidden md:block"
>
{formatDate(blog.date)}
</Card.Eyebrow>
</article>
);
};

View File

@@ -0,0 +1,36 @@
import { useState } from "react";
import { BlogItem } from "./BlogItem";
const BLOGS_LOAD_COUNT = 10;
// TODO types
export const BlogsList: React.FC<any> = ({ blogs }) => {
const [blogsCount, setBlogsCount] = useState(BLOGS_LOAD_COUNT);
const handleLoadMore = () => {
setBlogsCount((prevCount) => prevCount + BLOGS_LOAD_COUNT);
};
return (
<>
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div className="flex flex-col space-y-16">
{blogs.slice(0, blogsCount).map((blog) => {
return <BlogItem key={blog.urlPath} blog={blog} />;
})}
</div>
</div>
{blogs.length > blogsCount && (
<div className="text-center pt-20">
<button
onClick={handleLoadMore}
type="button"
className="inline-flex items-center rounded border border-gray-300 px-2.5 py-1.5 text-xs font-medium text-gray-200 shadow-sm hover:bg-gray-50/10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Show more
</button>
</div>
)}
</>
);
};

View File

@@ -0,0 +1 @@
export { BlogsList } from "./BlogsList";

View File

@@ -0,0 +1,38 @@
/* eslint import/no-default-export: off */
import { formatDate } from "../../utils/formatDate";
import { Avatar } from "../Avatar";
// TODO
type Props = any;
export const BlogLayout: React.FC<Props> = ({ children, ...frontMatter }) => {
const { title, date, authors } = frontMatter;
return (
<article className="docs prose prose-a:text-primary dark:prose-a:text-primary-dark prose-strong:text-primary dark:prose-strong:text-primary-dark prose-code:text-primary dark:prose-code:text-primary-dark prose-headings:text-primary dark:prose-headings:text-primary-dark prose text-primary dark:text-primary-dark prose-headings:font-headings dark:prose-invert prose-a:break-words mx-auto p-6">
<header>
<div className="mb-4 flex-col items-center">
{title && <h1 className="flex justify-center">{title}</h1>}
{date && (
<p className="text-sm text-zinc-400 dark:text-zinc-500 flex justify-center">
<time dateTime={date}>{formatDate(date)}</time>
</p>
)}
{authors && (
<div className="flex flex-wrap not-prose items-center space-x-6 space-y-3 justify-center">
{authors.map(({ name, avatar, urlPath }) => (
<Avatar
key={urlPath || name}
name={name}
img={avatar}
href={urlPath ? `/${urlPath}` : undefined}
/>
))}
</div>
)}
</div>
</header>
<section>{children}</section>
</article>
);
};

View File

@@ -0,0 +1 @@
export { BlogLayout } from "./BlogLayout";

View File

@@ -0,0 +1,170 @@
// import Link from 'next/link'
import clsx from "clsx";
import { ChevronRightIcon } from "../Icons";
interface CardProps extends React.PropsWithChildren {
as?: React.ElementType;
className?: string;
}
interface CardLinkProps extends React.PropsWithChildren {
href?: string;
className?: string;
}
interface CardTitleProps extends React.PropsWithChildren {
as?: React.ElementType;
href?: string;
className?: string;
}
interface CardDescriptionProps extends React.PropsWithChildren {
className?: string;
}
interface CardCtaProps extends React.PropsWithChildren {
className?: string;
}
interface CardEyebrowProps extends React.PropsWithChildren {
as?: React.ElementType;
decorate?: boolean;
className?: string;
[x: string]: unknown;
}
type Card = React.FC<CardProps> & { Link: React.FC<CardLinkProps> } & {
Title: React.FC<CardTitleProps>;
} & { Description: React.FC<CardDescriptionProps> } & {
Cta: React.FC<CardCtaProps>;
} & { Eyebrow: React.FC<CardEyebrowProps> };
export const Card: Card = ({ children, as: Component = "div", className }) => {
return (
<Component
className={clsx(className, "group relative flex flex-col items-start")}
>
{children}
</Component>
);
};
Card.Link = function CardLink({ children, href, className, ...props }) {
// <Link {...props}>
// <span className="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
// <span className="relative z-10">{children}</span>
// </Link>
return (
<>
<div className="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-slate-800/75 sm:-inset-x-6 sm:rounded-2xl" />
<a href={href} className={className} {...props}>
<span className="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span className="relative z-10">{children}</span>
</a>
</>
);
};
Card.Title = function CardTitle({
as: Component = "h2",
href,
children,
className,
}) {
return (
<Component
className={clsx(
className,
"text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
)}
>
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
</Component>
);
};
Card.Description = function CardDescription({ children, className }) {
return (
<p
className={clsx(
className,
"relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400"
)}
>
{children}
</p>
);
};
Card.Cta = function CardCta({ children, className }) {
return (
<div
aria-hidden="true"
className={clsx(
className,
"relative z-10 mt-4 flex items-center text-sm font-medium text-secondary dark:text-secondary-dark"
)}
>
{children}
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
</div>
);
};
/* Card.Avatar = function CardAvatar({ name, src, href }) {
* return (
* <a href={href} className="group block flex-shrink-0 mt-2">
* <div className="flex items-center">
* <div>
* {src ? (
* <img
* className="inline-block h-9 w-9 rounded-full"
* src={src}
* alt={name}
* />
* ) : (
* <span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-500">
* <span className="text-xs font-medium leading-none text-white">
* {initialsFromName(name)}
* </span>
* </span>
* )}
* </div>
* <div className="ml-3">
* <p className="text-sm font-medium text-gray-700 group-hover:text-gray-900">
* {name}
* </p>
* </div>
* </div>
* </a>
* );
* }; */
Card.Eyebrow = function CardEyebrow({
as: Component = "p",
decorate = false,
className,
children,
...props
}) {
return (
<Component
className={clsx(
className,
"relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500",
decorate && "pl-3.5"
)}
{...props}
>
{decorate && (
<span
className="absolute inset-y-0 left-0 flex items-center"
aria-hidden="true"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span>
)}
{children}
</Component>
);
};

View File

@@ -0,0 +1 @@
export { Card } from "./Card";

View File

@@ -0,0 +1,25 @@
import { DiscussionEmbed } from "disqus-react";
export interface DisqusConfig {
provider: "disqus";
pages?: Array<string>;
config: {
shortname: string;
};
}
export type DisqusProps = DisqusConfig["config"] & {
slug?: string;
};
export const Disqus: React.FC<DisqusProps> = ({ shortname, slug }) => {
return (
<DiscussionEmbed
shortname={shortname}
config={{
url: window?.location?.href,
identifier: slug,
}}
/>
);
};

View File

@@ -0,0 +1,53 @@
import Giscus, { BooleanString, Mapping, Repo } from "@giscus/react";
import { useTheme } from "next-themes";
export interface GiscusConfig {
provider: "giscus";
pages?: Array<string>;
config: {
theme?: string;
mapping: Mapping;
repo: Repo;
repositoryId: string;
category: string;
categoryId: string;
reactions: BooleanString;
metadata: BooleanString;
inputPosition?: string;
lang?: string;
};
}
export type GiscusProps = GiscusConfig["config"];
export const GiscusReactComponent: React.FC<GiscusProps> = ({
repo,
repositoryId,
category,
categoryId,
reactions = "0",
metadata = "0",
mapping = "pathname",
theme = "light",
}) => {
const { theme: nextTheme, resolvedTheme } = useTheme();
const commentsTheme =
nextTheme === "dark" || resolvedTheme === "dark"
? "transparent_dark"
: theme;
return (
<Giscus
repo={repo}
repoId={repositoryId}
category={category}
categoryId={categoryId}
mapping={mapping}
inputPosition="top"
reactionsEnabled={reactions}
emitMetadata={metadata}
// TODO: remove transparent_dark after theme toggle fix
theme={nextTheme ? commentsTheme : "transparent_dark"}
/>
);
};

View File

@@ -0,0 +1,59 @@
import { useEffect, useCallback } from "react";
import { useTheme } from "next-themes";
export interface UtterancesConfig {
provider: "utterances";
pages?: Array<string>;
config: {
theme?: string;
repo: string;
label: string;
issueTerm: string;
};
}
export type UtterancesProps = UtterancesConfig["config"];
export const Utterances: React.FC<UtterancesProps> = ({
repo,
label = "comments",
issueTerm = "pathname",
theme = "github-light",
}) => {
const { theme: nextTheme, resolvedTheme } = useTheme();
// TODO: remove preferred-color-scheme after theme toggle fix
const commentsTheme = nextTheme
? nextTheme === "dark" || resolvedTheme === "dark"
? "github-dark"
: theme
: "preferred-color-scheme";
const COMMENTS_ID = "comments-container";
const LoadComments = useCallback(() => {
const script = document.createElement("script");
script.src = "https://utteranc.es/client.js";
script.setAttribute("repo", repo);
script.setAttribute("issue-term", issueTerm);
script.setAttribute("label", label);
script.setAttribute("theme", commentsTheme);
script.setAttribute("crossorigin", "anonymous");
script.async = true;
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.appendChild(script);
return () => {
const comments = document.getElementById(COMMENTS_ID);
if (comments) comments.innerHTML = "";
};
}, [commentsTheme, issueTerm]);
// Reload on theme change
useEffect(() => {
LoadComments();
}, [LoadComments]);
// Added `relative` to fix a weird bug with `utterances-frame` position
return <div className="utterances-frame relative" id={COMMENTS_ID} />;
};

View File

@@ -0,0 +1,53 @@
import dynamic from "next/dynamic.js";
import { GiscusReactComponent, GiscusConfig, GiscusProps } from "./Giscus";
import { Utterances, UtterancesConfig, UtterancesProps } from "./Utterances";
import { Disqus, DisqusConfig, DisqusProps } from "./Disqus";
export type CommentsConfig = GiscusConfig | UtterancesConfig | DisqusConfig;
export interface CommentsProps {
commentsConfig: CommentsConfig;
slug?: string;
}
const GiscusComponent = dynamic<GiscusProps>(
() => {
return import("./Giscus").then((mod) => mod.GiscusReactComponent);
},
{ ssr: false }
);
const UtterancesComponent = dynamic<UtterancesProps>(
() => {
return import("./Utterances").then((mod) => mod.Utterances);
},
{ ssr: false }
);
const DisqusComponent = dynamic<DisqusProps>(
() => {
return import("./Disqus").then((mod) => mod.Disqus);
},
{ ssr: false }
);
export const Comments = ({ commentsConfig, slug }: CommentsProps) => {
switch (commentsConfig.provider) {
case "giscus":
return <GiscusComponent {...commentsConfig.config} />;
case "utterances":
return <UtterancesComponent {...commentsConfig.config} />;
case "disqus":
return <DisqusComponent slug={slug} {...commentsConfig.config} />;
}
};
export { GiscusReactComponent, Utterances, Disqus };
export type {
GiscusConfig,
GiscusProps,
UtterancesConfig,
UtterancesProps,
DisqusConfig,
DisqusProps,
};

View File

@@ -0,0 +1,22 @@
/* eslint import/no-default-export: off */
import { formatDate } from "../../utils/formatDate";
// TODO types
export const DocsLayout: React.FC<any> = ({ children, ...frontMatter }) => {
const { title, created } = frontMatter;
return (
<article className="docs prose prose-a:text-primary dark:prose-a:text-primary-dark prose-strong:text-primary dark:prose-strong:text-primary-dark prose-code:text-primary dark:prose-code:text-primary-dark prose-headings:text-primary dark:prose-headings:text-primary-dark prose text-primary dark:text-primary-dark dark:prose-invert prose-headings:font-headings prose-a:break-words mx-auto">
<header>
<div className="mb-6">
{created && (
<p className="text-sm text-zinc-400 dark:text-zinc-500">
<time dateTime={created}>{formatDate(created)}</time>
</p>
)}
{title && <h1>{title}</h1>}
</div>
</header>
<section>{children}</section>
</article>
);
};

View File

@@ -0,0 +1 @@
export { DocsLayout } from "./Docs";

View File

@@ -0,0 +1,12 @@
export const ChevronRightIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M6.75 5.75 9.25 8l-2.5 2.25"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

View File

@@ -0,0 +1,14 @@
export const CloseIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
{...props}
>
<path d="M5 5l14 14M19 5l-14 14" />
</svg>
);
};

View File

@@ -0,0 +1,14 @@
export const DiscordIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
</svg>
);
};

View File

@@ -0,0 +1,7 @@
export const GitHubIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
};

View File

@@ -0,0 +1,14 @@
export const MenuIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
{...props}
>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
};

View File

@@ -0,0 +1,7 @@
export const SearchIcon: React.FC<{ [x: string]: unknown }> = (props) => {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
</svg>
);
};

View File

@@ -0,0 +1,6 @@
export { GitHubIcon } from "./GitHubIcon";
export { DiscordIcon } from "./DiscordIcon";
export { MenuIcon } from "./MenuIcon";
export { CloseIcon } from "./CloseIcon";
export { SearchIcon } from "./SearchIcon";
export { ChevronRightIcon } from "./ChevronRightIcon";

View File

@@ -0,0 +1,30 @@
export const EditThisPage = ({ url }: { url: string }) => {
return (
<div className="mb-10 prose dark:prose-invert p-6 mx-auto">
<a
className="flex no-underline font-semibold justify-center"
href={url}
target="_blank"
rel="noopener noreferrer"
>
Edit this page
<span className="mx-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</span>
</a>
</div>
);
};

View File

@@ -0,0 +1,61 @@
import Link from "next/link.js";
import { AuthorConfig, NavLink } from "../types";
interface Props {
links: Array<NavLink>;
author: AuthorConfig;
}
export const Footer: React.FC<Props> = ({ links, author }) => {
return (
<footer className="bg-background dark:bg-background-dark prose dark:prose-invert max-w-none flex flex-col items-center justify-center w-full h-auto pt-10 pb-20">
<div className="flex w-full flex-wrap justify-center">
{links.map((item) => (
<Link
key={item.href}
href={item.href}
className="inline-flex items-center mx-4 px-1 pt-1 font-regular hover:text-slate-300 no-underline"
>
{/* TODO aria-current={item.current ? "page" : undefined} */}
{item.name}
</Link>
))}
</div>
<p className="flex items-center justify-center">
Created by
<a
href={author.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center no-underline"
>
{author.logo && (
<img
src={author.logo}
alt={author.name}
className="my-0 mx-1 h-6 block"
/>
)}
{author.name}
</a>
</p>
<p className="flex items-center justify-center">
Made with
<a
href="https://flowershow.app/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center no-underline"
>
<img
src="https://flowershow.app/images/logo.svg"
alt="Flowershow"
className="my-0 mx-1 h-6 block"
/>
Flowershow
</a>
</p>
</footer>
);
};

View File

@@ -0,0 +1,147 @@
import { useEffect, useState } from "react";
import Head from "next/head.js";
import { NextRouter, useRouter } from "next/router.js";
import clsx from "clsx";
import { useTableOfContents } from "./useTableOfContents";
import { collectHeadings } from "../../utils";
import { Nav } from "../Nav";
import { SiteToc, NavItem, NavGroup } from "../SiteToc";
import { Comments, CommentsConfig } from "../Comments";
import { Footer } from "./Footer";
import { EditThisPage } from "./EditThisPage";
import { TableOfContents, TocSection } from "./TableOfContents";
import { NavConfig, ThemeConfig } from "../Nav";
import { AuthorConfig } from "../types";
interface Props extends React.PropsWithChildren {
showComments: boolean;
showEditLink: boolean;
showSidebar: boolean;
showToc: boolean;
nav: NavConfig;
author: AuthorConfig;
theme: ThemeConfig;
urlPath: string;
commentsConfig: CommentsConfig;
siteMap: Array<NavItem | NavGroup>;
editUrl?: string;
}
export const Layout: React.FC<Props> = ({
children,
nav,
author,
theme,
showEditLink,
showToc,
showSidebar,
urlPath,
showComments,
commentsConfig,
editUrl,
siteMap,
}) => {
const [isScrolled, setIsScrolled] = useState(false);
const [tableOfContents, setTableOfContents] = useState<TocSection[]>([]);
const currentSection = useTableOfContents(tableOfContents);
const router: NextRouter = useRouter();
useEffect(() => {
if (!showToc) return;
const headingNodes: NodeListOf<HTMLHeadingElement> =
document.querySelectorAll("h1,h2,h3");
const toc = collectHeadings(headingNodes);
setTableOfContents(toc ?? []);
}, [router.asPath, showToc]); // update table of contents on route change with next/link
useEffect(() => {
function onScroll() {
setIsScrolled(window.scrollY > 0);
}
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return (
<>
<Head>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💐</text></svg>"
/>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<div className="min-h-screen bg-background dark:bg-background-dark">
{/* NAVBAR */}
<div
className={clsx(
"sticky top-0 z-50 w-full",
isScrolled
? "dark:bg-background-dark/95 bg-background/95 backdrop-blur [@supports(backdrop-filter:blur(0))]:dark:bg-background-dark/75"
: "dark:bg-background-dark bg-background"
)}
>
<div className="max-w-8xl mx-auto p-4 md:px-8">
<Nav
title={nav.title}
logo={nav.logo}
links={nav.links}
search={nav.search}
social={nav.social}
defaultTheme={theme.defaultTheme}
themeToggleIcon={theme.themeToggleIcon}
>
{showSidebar && <SiteToc currentPath={urlPath} nav={siteMap} />}
</Nav>
</div>
</div>
{/* wrapper for sidebar, main content and ToC */}
<div
className={clsx(
"max-w-8xl mx-auto px-4 md:px-8",
showSidebar && "lg:ml-[18rem]",
showToc && "xl:mr-[18rem]"
)}
>
{/* SIDEBAR */}
{showSidebar && (
<div className="hidden lg:block fixed z-20 w-[18rem] top-[4.6rem] right-auto bottom-0 left-[max(0px,calc(50%-44rem))] pt-8 pl-8 overflow-y-auto">
<SiteToc currentPath={urlPath} nav={siteMap} />
</div>
)}
{/* MAIN CONTENT & FOOTER */}
<main className="mx-auto pt-8">
{children}
{/* EDIT THIS PAGE LINK */}
{showEditLink && editUrl && <EditThisPage url={editUrl} />}
{/* PAGE COMMENTS */}
{showComments && (
<div
className="prose mx-auto pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"
id="comment"
>
{<Comments commentsConfig={commentsConfig} slug={urlPath} />}
</div>
)}
</main>
<Footer links={nav.links} author={author} />
{/** TABLE OF CONTENTS */}
{showToc && tableOfContents.length > 0 && (
<div className="hidden xl:block fixed z-20 w-[18rem] top-[4.6rem] bottom-0 right-[max(0px,calc(50%-44rem))] left-auto pt-8 pr-8 overflow-y-auto">
<TableOfContents
tableOfContents={tableOfContents}
currentSection={currentSection}
/>
</div>
)}
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,90 @@
import Link from "next/link.js";
export interface TocSection {
id: string;
title: string;
level: string;
children?: any;
}
interface Props {
tableOfContents: TocSection[];
currentSection: string;
}
export const TableOfContents: React.FC<Props> = ({
tableOfContents,
currentSection,
}) => {
function isActiveSection(section) {
if (section.id === currentSection) {
return true;
}
if (!section.children) {
return false;
}
return section.children.findIndex(isActiveSection) > -1;
}
return (
<nav aria-labelledby="on-this-page-title">
<h2 className="font-display text-md font-medium text-slate-900 dark:text-white">
On this page
</h2>
<ol className="mt-4 space-y-3 text-sm">
{tableOfContents.map((section) => (
<li key={section.id}>
<h3>
<Link
href={`#${section.id}`}
className={
isActiveSection(section)
? "text-secondary dark:text-secondary-dark"
: "font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
}
>
{section.title}
</Link>
</h3>
{section.children && section.children.length > 0 && (
<ol className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
{section.children.map((subSection) => (
<li key={subSection.id}>
<Link
href={`#${subSection.id}`}
className={
isActiveSection(subSection)
? "text-secondary dark:text-secondary-dark"
: "hover:text-slate-600 dark:hover:text-slate-300"
}
>
{subSection.title}
</Link>
{subSection.children && subSection.children.length > 0 && (
<ol className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
{subSection.children.map((thirdSection) => (
<li key={thirdSection.id}>
<Link
href={`#${thirdSection.id}`}
className={
isActiveSection(thirdSection)
? "text-secondary dark:text-secondary-dark"
: "hover:text-slate-600 dark:hover:text-slate-300"
}
>
{thirdSection.title}
</Link>
</li>
))}
</ol>
)}
</li>
))}
</ol>
)}
</li>
))}
</ol>
</nav>
);
};

View File

@@ -0,0 +1,5 @@
export { EditThisPage } from "./EditThisPage";
export { Layout } from "./Layout";
export { useTableOfContents } from "./useTableOfContents";
export { TableOfContents, TocSection } from "./TableOfContents";
export { Footer } from "./Footer";

View File

@@ -0,0 +1,51 @@
import { useCallback, useEffect, useState } from "react";
// TODO types
export const useTableOfContents = (tableOfContents) => {
const [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id);
const getHeadings = useCallback((toc) => {
return toc
.flatMap((node) => [
node.id,
...node.children.flatMap((child) => [
child.id,
...child.children.map((subChild) => subChild.id),
]),
])
.map((id) => {
const el = document.getElementById(id);
if (!el) return null;
const style = window.getComputedStyle(el);
const scrollMt = parseFloat(style.scrollMarginTop);
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
return { id, top };
})
.filter((el) => !!el);
}, []);
useEffect(() => {
if (tableOfContents.length === 0) return;
const headings = getHeadings(tableOfContents);
function onScroll() {
const top = window.scrollY + 4.5;
let current = headings[0].id;
headings.forEach((heading) => {
if (top >= heading.top) {
current = heading.id;
}
return current;
});
setCurrentSection(current);
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => {
window.removeEventListener("scroll", onScroll);
};
}, [getHeadings, tableOfContents]);
return currentSection;
};

View File

@@ -0,0 +1,25 @@
import dynamic from "next/dynamic.js";
// import { useTheme } from "next-themes";
import type { MermaidProps } from "mdx-mermaid/lib/Mermaid";
// import type { Config } from "mdx-mermaid/lib/config.model";
const MdxMermaid = dynamic(
() => import("mdx-mermaid/lib/Mermaid").then((res) => res.Mermaid),
{ ssr: false }
);
export const Mermaid: React.FC<MermaidProps> = ({ ...props }) => {
// TODO: add light and dark theme configs
// currently Mermaid component doesn't render if configs are passed as props.
// const { theme } = useTheme()
// const config: Config = {
// mermaid: {
// fontFamily: "inherit",
// theme: theme
// }
// }
return <MdxMermaid {...props} />;
};

View File

@@ -0,0 +1 @@
export { Mermaid } from "./Mermaid";

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { ThemeSelector } from "../Base";
import { SearchContext, SearchField } from "../Search";
import { NavMobile } from "./NavMobile";
import { NavItem } from "./NavItem";
import { NavTitle } from "./NavTitle";
import { NavSocial } from "./NavSocial";
import { NavLink, SocialLink, SearchProviderConfig } from "../types";
export interface ThemeConfig {
defaultTheme: "dark" | "light";
themeToggleIcon: string;
}
export interface NavConfig {
title: string;
logo?: string;
version?: string;
links: Array<NavLink>;
search?: SearchProviderConfig;
social?: Array<SocialLink>;
}
interface Props extends NavConfig, ThemeConfig, React.PropsWithChildren {}
export const Nav: React.FC<Props> = ({
children,
title,
logo,
version,
links,
search,
social,
defaultTheme,
themeToggleIcon,
}) => {
const [modifierKey, setModifierKey] = useState<string>();
const [Search, setSearch] = useState<any>(); // TODO types
useEffect(() => {
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
setModifierKey(isMac ? "⌘" : "Ctrl ");
}, []);
useEffect(() => {
if (search) {
setSearch(SearchContext(search.provider));
}
}, [search]);
return (
<nav className="flex justify-between">
{/* Mobile navigation */}
<div className="mr-2 sm:mr-4 flex lg:hidden">
<NavMobile links={links}>{children}</NavMobile>
</div>
{/* Non-mobile navigation */}
<div className="flex flex-none items-center">
<NavTitle title={title} logo={logo} version={version} />
{links && (
<div className="hidden lg:flex ml-8 mr-6 sm:mr-8 md:mr-0">
{links.map((link) => (
<NavItem link={link} key={link.name} />
))}
</div>
)}
</div>
{/* Search field and social links */}
<div className="relative flex items-center basis-auto justify-end gap-6 xl:gap-8 md:shrink w-full">
{Search && (
<Search>
{({ query }: any) => (
<SearchField modifierKey={modifierKey} onOpen={query?.toggle} />
)}
</Search>
)}
<ThemeSelector
defaultTheme={defaultTheme}
toggleIcon={themeToggleIcon}
/>
{social && <NavSocial links={social} />}
</div>
</nav>
);
};

View File

@@ -0,0 +1,21 @@
import { Menu } from "@headlessui/react";
import Link from "next/link.js";
import { BaseLink } from "../Base";
import { NavLink } from "../types";
interface Props {
link: NavLink;
}
export const NavItem: React.FC<Props> = ({ link }) => {
return (
<Menu as="div" className="relative">
<Link
href={link.href}
className="text-slate-500 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-600"
>
{link.name}
</Link>
</Menu>
);
};

View File

@@ -0,0 +1,116 @@
import { Dialog, Menu } from "@headlessui/react";
import Link from "next/link.js";
import { useRouter } from "next/router.js";
import { useEffect, useState } from "react";
import { SearchContext, SearchField } from "../Search";
import { MenuIcon, CloseIcon } from "../Icons";
import { NavLink, SearchProviderConfig } from "../types";
interface Props extends React.PropsWithChildren {
author?: string;
links?: Array<NavLink>;
search?: SearchProviderConfig;
}
// TODO why mobile navigation only accepts author and regular nav accepts different things like title, logo, version
export const NavMobile: React.FC<Props> = ({
children,
links,
search,
author,
}) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [Search, setSearch] = useState<any>(); // TODO types
useEffect(() => {
if (!isOpen) return;
function onRouteChange() {
setIsOpen(false);
}
router.events.on("routeChangeComplete", onRouteChange);
router.events.on("routeChangeError", onRouteChange);
return () => {
router.events.off("routeChangeComplete", onRouteChange);
router.events.off("routeChangeError", onRouteChange);
};
}, [router, isOpen]);
useEffect(() => {
if (search) {
setSearch(SearchContext(search.provider));
}
}, [search]);
return (
<>
<button
type="button"
onClick={() => setIsOpen(true)}
className="relative"
aria-label="Open navigation"
>
<MenuIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Dialog
open={isOpen}
onClose={setIsOpen}
className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-background-dark/50 pr-10 backdrop-blur lg:hidden"
aria-label="Navigation"
>
<Dialog.Panel className="relative min-h-full w-full max-w-xs bg-background px-4 pt-5 pb-12 dark:bg-background-dark sm:px-6">
<div className="flex items-center mb-6">
<button
type="button"
onClick={() => setIsOpen(false)}
aria-label="Close navigation"
>
<CloseIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Link
href="/"
className="ml-6"
aria-label="Home page"
legacyBehavior
>
{/* <Logomark className="h-9 w-9" /> */}
<div className="font-extrabold text-primary dark:text-primary-dark text-2xl ml-6">
{author}
</div>
</Link>
</div>
{Search && (
<Search>
{({ query }: any) => <SearchField mobile onOpen={query.toggle} />}
</Search>
)}
{links && (
<ul className="mt-2 space-y-2 border-l-2 border-slate-100 dark:border-slate-800 lg:mt-4 lg:space-y-4 lg:border-slate-200">
{links.map((link) => (
<Menu as="div" key={link.name} className="relative">
<Menu.Button>
<li key={link.href}>
<Link
href={link.href}
className={`
block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300`}
>
{link.name}
</Link>
</li>
</Menu.Button>
</Menu>
))}
</ul>
)}
{/* <div className="pt-6 border border-t-2">
{children}
</div> */}
</Dialog.Panel>
</Dialog>
</>
);
};

View File

@@ -0,0 +1,27 @@
import Link from "next/link.js";
import { GitHubIcon, DiscordIcon } from "../Icons";
import { SocialLink, SocialPlatform } from "../types";
interface Props {
links: Array<SocialLink>;
}
const icons: { [K in SocialPlatform]: React.FC<any> } = {
github: GitHubIcon,
discord: DiscordIcon,
};
export const NavSocial: React.FC<Props> = ({ links }) => {
return (
<>
{links.map(({ label, href }) => {
const Icon = icons[label];
return (
<Link key={label} href={href} aria-label={label} className="group">
<Icon className="h-6 w-6 dark:fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
</Link>
);
})}
</>
);
};

View File

@@ -0,0 +1,27 @@
import Link from "next/link.js";
interface Props {
title: string;
logo?: string;
version?: string;
}
export const NavTitle: React.FC<Props> = ({ title, logo, version }) => {
return (
<Link
href="/"
aria-label="Home page"
className="flex items-center font-extrabold text-xl sm:text-2xl text-slate-900 dark:text-white"
>
{logo && (
<img src={logo} alt={title} className="nav-logo mr-1 fill-white" />
)}
{title && <span>{title}</span>}
{version && (
<div className="mx-2 rounded-full border border-slate-500 py-1 px-3 text-xs text-slate-500">
{version}
</div>
)}
</Link>
);
};

View File

@@ -0,0 +1 @@
export { Nav, NavConfig, ThemeConfig } from "./Nav";

View File

@@ -0,0 +1,74 @@
import { useRef, useState } from "react";
interface Props extends React.PropsWithChildren {
className?: string;
}
export const Pre: React.FC<Props> = ({ children, ...props }) => {
const ref = useRef<any>(); // TODO type
const [hovered, setHovered] = useState(false);
const [copied, setCopied] = useState(false);
const onEnter = () => {
setHovered(true);
};
const onExit = () => {
setHovered(false);
setCopied(false);
};
const onCopy = () => {
setCopied(true);
navigator.clipboard.writeText(ref.current.textContent);
setTimeout(() => {
setCopied(false);
}, 2000);
};
return (
<div
ref={ref}
onMouseEnter={onEnter}
onMouseLeave={onExit}
className="relative"
>
{hovered && (
<button
aria-label="Copy code"
type="button"
className={`absolute right-2 top-2 h-6 w-6 rounded border bg-gray-700 p-1 ease-in-out duration-100 ${
copied
? "border-green-400 focus:border-green-400 focus:outline-none"
: "border-slate-300"
}`}
onClick={onCopy}
>
<svg
aria-hidden="true"
viewBox="-2 -2 20 20"
fill="currentColor"
className={copied ? "text-green-400" : "text-slate-300"}
>
{copied ? (
<path
fillRule="evenodd"
d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
/>
) : (
<>
<path
fillRule="evenodd"
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
/>
<path
fillRule="evenodd"
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
/>
</>
)}
</svg>
</button>
)}
<pre>{children}</pre>
</div>
);
};

View File

@@ -0,0 +1 @@
export { Pre } from "./Pre";

View File

@@ -0,0 +1,132 @@
import * as docsearch from "@docsearch/react";
import Head from "next/head.js";
import Link from "next/link.js";
import { useRouter } from "next/router.js";
import { createContext, useCallback, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
const { useDocSearchKeyboardEvents } = docsearch;
let DocSearchModal: any = null;
function Hit({ hit, children }) {
return <Link href={hit.url}>{children}</Link>;
}
export const AlgoliaSearchContext = createContext({});
export function AlgoliaSearchProvider({ children, config }) {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [initialQuery, setInitialQuery] = useState(undefined);
const importDocSearchModalIfNeeded = useCallback(async () => {
if (DocSearchModal) {
return Promise.resolve();
}
const [{ DocSearchModal: Modal }] = await Promise.all([docsearch]);
// eslint-disable-next-line
DocSearchModal = Modal;
}, [DocSearchModal]);
const onOpen = useCallback(() => {
importDocSearchModalIfNeeded().then(() => {
setIsOpen(true);
});
}, [importDocSearchModalIfNeeded, setIsOpen]);
const onClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
const onInput = useCallback(
(event) => {
importDocSearchModalIfNeeded().then(() => {
setIsOpen(true);
setInitialQuery(event.key);
});
},
[importDocSearchModalIfNeeded, setIsOpen, setInitialQuery]
);
// web accessibility
// https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/keyboard-navigation/
const navigator = useRef({
navigate({ itemUrl }) {
// Algolia results could contain URL's from other domains which cannot
// be served through history and should navigate with window.location
const isInternalLink = itemUrl.startsWith("/");
const isAnchorLink = itemUrl.startsWith("#");
if (!isInternalLink && !isAnchorLink) {
window.location.href = itemUrl;
} else {
router.push(itemUrl);
}
},
}).current;
// https://docsearch.algolia.com/docs/api#transformitems
const transformItems = (items) =>
items.map((item) => {
// If Algolia contains a external domain, we should navigate without
// relative URL
const isInternalLink = item.url.startsWith("/");
const isAnchorLink = item.url.startsWith("#");
if (!isInternalLink && !isAnchorLink) {
return item;
}
// We transform the absolute URL into a relative URL.
const url = new URL(item.url);
return {
...item,
// url: withBaseUrl(`${url.pathname}${url.hash}`),
url: `${url.pathname}${url.hash}`,
};
});
// ).current;
useDocSearchKeyboardEvents({
isOpen,
onOpen,
onClose,
onInput,
});
const providerValue = useMemo(
() => ({ query: { setSearch: setInitialQuery, toggle: onOpen } }),
[setInitialQuery, onOpen]
);
return (
<AlgoliaSearchContext.Provider value={providerValue}>
<Head>
{/* This hints the browser that the website will load data from Algolia,
and allows it to preconnect to the DocSearch cluster. It makes the first
query faster, especially on mobile. */}
<link
rel="preconnect"
href={`https://${config.appId}-dsn.algolia.net`}
crossOrigin="anonymous"
/>
</Head>
{children}
{isOpen &&
DocSearchModal &&
createPortal(
<DocSearchModal
onClose={onClose}
initialScrollY={window.scrollY}
initialQuery={initialQuery}
navigator={navigator}
transformItems={transformItems}
hitComponent={Hit}
placeholder={config.placeholder ?? "Search"}
{...config}
/>,
document.body
)}
</AlgoliaSearchContext.Provider>
);
}

View File

@@ -0,0 +1,33 @@
import router from "next/router.js";
import { Action } from "kbar";
import { KBarModal } from "./KBarModal";
export const KBarSearchProvider = ({ config, children }) => {
const defaultActions = config?.defaultActions;
const searchDocumentsPath = "/search.json";
let startingActions: Action[] = [
{
id: "homepage",
name: "Homepage",
keywords: "",
section: "Home",
perform: () => router.push("/"),
},
];
if (defaultActions && Array.isArray(defaultActions))
startingActions = [...startingActions, ...defaultActions];
return KBarModal ? (
<KBarModal
startingActions={startingActions}
searchDocumentsPath={searchDocumentsPath}
>
{children}
</KBarModal>
) : (
children
);
};

View File

@@ -0,0 +1,21 @@
import { KBarProvider, Action } from "kbar";
import { Portal } from "./KBarPortal";
interface Props extends React.PropsWithChildren {
searchDocumentsPath: string;
startingActions?: Action[];
}
export const KBarModal: React.FC<Props> = ({
searchDocumentsPath,
startingActions,
children,
}) => {
return (
<KBarProvider actions={startingActions}>
<Portal searchDocumentsPath={searchDocumentsPath} />
{children}
</KBarProvider>
);
};

View File

@@ -0,0 +1,120 @@
import { useEffect, useState } from "react";
import {
Action,
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
useRegisterActions,
} from "kbar";
import { kbarActionsFromDocuments } from "./kbarActionsFromDocuments";
interface Props {
searchDocumentsPath: string;
}
export const Portal: React.FC<Props> = ({ searchDocumentsPath }) => {
const [searchActions, setSearchActions] = useState<Action[]>([]);
useEffect(() => {
const fetchData = async () => {
const res = await fetch(searchDocumentsPath);
const json = await res.json();
const actions = kbarActionsFromDocuments(json);
setSearchActions(actions);
};
fetchData();
}, [searchDocumentsPath]);
useRegisterActions(searchActions, [searchActions]);
return (
<KBarPortal>
<KBarPositioner className="bg-gray-300/50 p-4 backdrop-blur backdrop-filter dark:bg-black/50">
<KBarAnimator className="w-full max-w-xl">
<div className="overflow-hidden rounded-2xl border border-gray-100 bg-gray-50 dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center space-x-4 p-4">
<span className="block w-5">
<svg
className="text-gray-400 dark:text-gray-300"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</span>
<KBarSearch
defaultPlaceholder="Search"
className="h-8 w-full bg-transparent text-slate-600 placeholder-slate-400 focus:outline-none dark:text-slate-200 dark:placeholder-slate-500"
/>
<span className="inline-block whitespace-nowrap rounded border border-slate-400/70 px-1.5 align-middle font-medium leading-4 tracking-wide text-slate-500 [font-size:10px] dark:border-slate-600 dark:text-slate-400">
ESC
</span>
</div>
<RenderResults />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
};
function RenderItem(props) {
const { item, active } = props;
return (
<div
className={
typeof item === "string"
? ""
: "hover:bg-gray-200 hover:dark:bg-gray-800"
}
>
{typeof item === "string" ? (
<div className="pt-3">
<div className="text-primary-600 block border-t border-gray-100 px-4 pt-6 pb-2 text-xs font-semibold uppercase dark:border-gray-800">
{item}
</div>
</div>
) : (
<div
className={`block cursor-pointer px-4 py-2 text-gray-600 dark:text-gray-200 ${
active ? "bg-primary-600" : "bg-transparent"
}`}
>
{item?.subtitle && (
<div
className={`${
active ? "text-gray-200" : "text-gray-400 dark:text-gray-500"
} text-xs`}
>
{item.subtitle}
</div>
)}
<div>{item?.name}</div>
</div>
)}
</div>
);
}
function RenderResults() {
const { results } = useMatches();
if (results.length) {
return <KBarResults items={results} onRender={RenderItem} />;
}
return (
<div className="block border-t border-gray-100 px-4 py-8 text-center text-gray-400 dark:border-gray-800 dark:text-gray-600">
No results for your search...
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { SearchIcon } from "../Icons";
// TODO types
export const SearchField: React.FC<any> = (props) => {
const { modifierKey, onOpen, mobile } = props;
return (
<button
type="button"
className={`
group flex h-6 w-6 items-center justify-center
${
mobile
? "sm:hidden justify-start min-w-full flex-none rounded-lg px-4 py-5 my-6 text-sm ring-1 ring-slate-200 dark:bg-slate-800/75 dark:ring-inset dark:ring-white/5"
: "hidden sm:flex sm:justify-start md:h-auto md:w-auto xl:w-full max-w-[380px] shrink xl:rounded-lg xl:py-2.5 xl:pl-4 xl:pr-3.5 md:text-sm xl:ring-1 xl:ring-slate-200 xl:hover:ring-slate-300 dark:xl:bg-slate-800/75 dark:xl:ring-inset dark:xl:ring-white/5 dark:xl:hover:bg-slate-700/40 dark:xl:hover:ring-slate-500"
}
`}
onClick={onOpen}
>
<SearchIcon className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 dark:fill-slate-500 md:group-hover:fill-slate-400" />
<span
className={`
text-slate-500 dark:text-slate-400
${
mobile
? "w-full not-sr-only text-left ml-2"
: "hidden xl:block sr-only md:not-sr-only md:ml-2"
}
`}
>
Search
</span>
{modifierKey && (
<kbd
className={`
${
mobile
? "hidden"
: "ml-auto font-medium text-slate-400 dark:text-slate-500 hidden xl:block"
}
`}
>
<kbd className="font-sans">{modifierKey}</kbd>
<kbd className="font-sans">K</kbd>
</kbd>
)}
</button>
);
};

View File

@@ -0,0 +1,69 @@
import dynamic from "next/dynamic.js";
import {
SearchProvider as SearchProviderType,
SearchProviderConfig,
} from "../types";
const AlgoliaSearchProvider = dynamic(
async () => {
return await import("./Algolia").then((mod) => mod.AlgoliaSearchProvider);
},
{ ssr: false }
);
const AlgoliaSearchContext = dynamic(
async () => {
return await import("./Algolia").then(
(mod) => mod.AlgoliaSearchContext.Consumer
);
},
{ ssr: false }
);
const KBarProvider = dynamic(
async () => {
return await import("./KBar").then((mod) => mod.KBarSearchProvider);
},
{ ssr: false }
);
const KBarSearchContext = dynamic(
async () => {
return await import("kbar").then((mod) => mod.KBarContext.Consumer);
},
{ ssr: false }
);
export const SearchProvider = ({
searchConfig,
children,
}: {
searchConfig: SearchProviderConfig;
children: React.ReactNode;
}) => {
switch (searchConfig?.provider) {
case "algolia":
return (
<AlgoliaSearchProvider config={searchConfig.config}>
{children}
</AlgoliaSearchProvider>
);
case "kbar":
return (
<KBarProvider config={searchConfig.config}>{children}</KBarProvider>
);
default:
return <>{children}</>;
}
};
export const SearchContext = (provider: SearchProviderType) => {
switch (provider) {
case "algolia":
return AlgoliaSearchContext;
case "kbar":
return KBarSearchContext;
default:
return undefined;
}
};

View File

@@ -0,0 +1,3 @@
// TODO tidy up this API
export { SearchField } from "./SearchField";
export { SearchContext, SearchProvider } from "./SearchProvider";

View File

@@ -0,0 +1,26 @@
// TODO don't import router here?
import router from "next/router.js";
import { Action } from "kbar";
import { formatDate } from "../../utils/formatDate";
import { nameFromUrl } from "../../utils/nameFromUrl";
// TODO temp type
type Document = any;
export const kbarActionsFromDocuments = (docs: Document[]): Action[] => {
const actions: Action[] = [];
for (const doc of docs) {
// excluding home path as this is defined in starting actions
doc.url_path &&
actions.push({
id: doc.url_path,
name: doc.title ?? nameFromUrl(doc.url_path),
keywords: doc.description ?? "",
section: doc.sourceDir ?? "Page",
subtitle: doc.date && formatDate(doc.date, "en-US"),
perform: () => router.push(`/${doc.url_path}`),
});
}
return actions;
};

View File

@@ -0,0 +1,39 @@
import { forwardRef } from "react";
import clsx from "clsx";
const OuterContainer = forwardRef<
HTMLDivElement,
React.PropsWithChildren & { className?: string }
>(({ className, children, ...props }, ref) => {
return (
<div ref={ref} className={clsx("sm:px-8", className)} {...props}>
<div className="mx-auto max-w-5xl lg:px-8">{children}</div>
</div>
);
});
const InnerContainer = forwardRef<
HTMLDivElement,
React.PropsWithChildren & { className?: string }
>(({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={clsx("relative px-4 sm:px-8 lg:px-12", className)}
{...props}
>
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
</div>
);
});
export const Container = forwardRef<
HTMLDivElement,
React.PropsWithChildren & { className?: string }
>(({ children, ...props }, ref) => {
return (
<OuterContainer ref={ref} {...props}>
<InnerContainer>{children}</InnerContainer>
</OuterContainer>
);
});

View File

@@ -0,0 +1,20 @@
/* eslint import/no-default-export: off */
import { Container } from "./Container";
// TODO types
export const SimpleLayout: React.FC<any> = ({ children, ...frontMatter }) => {
const { title, description } = frontMatter;
return (
<Container className="my-16 sm:mt-32">
<header className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{title}
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
{description}
</p>
</header>
<div className="mt-16 sm:mt-20">{children}</div>
</Container>
);
};

View File

@@ -0,0 +1 @@
export { SimpleLayout } from "./SimpleLayout";

View File

@@ -0,0 +1,108 @@
import Link from "next/link.js";
import clsx from "clsx";
import { Disclosure, Transition } from "@headlessui/react";
export interface NavItem {
name: string;
href: string;
}
export interface NavGroup {
name: string;
path: string;
level: number;
children: Array<NavItem | NavGroup>;
}
interface Props {
currentPath: string;
nav: Array<NavItem | NavGroup>;
}
function isNavGroup(item: NavItem | NavGroup): item is NavGroup {
return (item as NavGroup).children !== undefined;
}
function navItemBeforeNavGroup(a, b) {
if (isNavGroup(a) === isNavGroup(b)) {
return 0;
}
if (isNavGroup(a) && !isNavGroup(b)) {
return 1;
}
return -1;
}
function sortNavGroupChildren(items: Array<NavItem | NavGroup>) {
return items.sort(
(a, b) => navItemBeforeNavGroup(a, b) || a.name.localeCompare(b.name)
);
}
export const SiteToc: React.FC<Props> = ({ currentPath, nav }) => {
function isActiveItem(item: NavItem) {
return item.href === currentPath;
}
return (
<nav data-testid="lhs-sidebar" className="flex flex-col space-y-3 text-sm">
{sortNavGroupChildren(nav).map((n) => (
<NavComponent item={n} isActive={false} />
))}
</nav>
);
};
const NavComponent: React.FC<{
item: NavItem | NavGroup;
isActive: boolean;
}> = ({ item, isActive }) => {
return !isNavGroup(item) ? (
<Link
key={item.name}
href={item.href}
className={clsx(
isActive
? "text-sky-500"
: "font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300",
"block"
)}
>
{item.name}
</Link>
) : (
<Disclosure as="div" key={item.name} className="flex flex-col space-y-3">
{({ open }) => (
<div>
<Disclosure.Button className="group w-full flex items-center text-left text-md font-medium text-slate-900 dark:text-white">
<svg
className={clsx(
open ? "text-slate-400 rotate-90" : "text-slate-300",
"h-3 w-3 mr-2 flex-shrink-0 transform transition-colors duration-150 ease-in-out group-hover:text-slate-400"
)}
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M6 6L14 10L6 14V6Z" fill="currentColor" />
</svg>
{item.name}
</Disclosure.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className="flex flex-col space-y-3 pl-5 mt-3">
{sortNavGroupChildren(item.children).map((subItem) => (
<NavComponent item={subItem} isActive={false} />
))}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
);
};

View File

@@ -0,0 +1 @@
export { SiteToc, NavItem, NavGroup } from "./SiteToc";

View File

@@ -0,0 +1,225 @@
import React, { useState, useEffect, useRef, Fragment } from "react";
// importing separately due to build error
// Module '"@floating-ui/react-dom-interactions"' has no exported member 'autoPlacement' ...
import {
arrow,
autoPlacement,
inline,
offset,
shift,
} from "@floating-ui/react-dom";
import {
FloatingPortal,
useDismiss,
useFloating,
useHover,
useFocus,
useInteractions,
useRole,
} from "@floating-ui/react-dom-interactions";
import { motion, AnimatePresence } from "framer-motion";
interface Props extends React.PropsWithChildren {
render: (t) => React.ReactNode;
href: string;
data: any;
usehook?: any;
className?: string;
}
const tooltipBoxStyle = (theme: string) =>
({
height: "auto",
maxWidth: "40rem",
padding: "1rem",
background: theme === "light" ? "#fff" : "#000",
color: theme === "light" ? "rgb(99, 98, 98)" : "#A8A8A8",
borderRadius: "4px",
boxShadow: "rgba(0, 0, 0, 0.55) 0px 0px 16px -3px",
} as React.CSSProperties);
const tooltipBodyStyle = (theme: string) =>
({
maxHeight: "4.8rem",
position: "relative",
lineHeight: "1.2rem",
overflow: "hidden",
} as React.CSSProperties);
const tooltipArrowStyle = ({ theme, x, y, side }) =>
({
position: "absolute",
left: x != null ? `${x}px` : "",
top: y != null ? `${y}px` : "",
right: "",
bottom: "",
[side]: "-4px",
height: "8px",
width: "8px",
background: theme === "light" ? "#fff" : "#000",
transform: "rotate(45deg)",
} as React.CSSProperties);
export const Tooltip: React.FC<Props> = ({
render,
data,
usehook,
...props
}) => {
const theme = "light"; // temporarily hard-coded; light theme tbd in next PR
const arrowRef = useRef(null);
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipData, setTooltipData] = useState({
content: null || <Fragment />,
image: "",
});
const [tooltipContentLoaded, setTooltipContentLoaded] = useState(false);
// floating-ui hook
const {
x,
y,
reference, // trigger element back ref
floating, // tooltip back ref
placement, // default: 'bottom'
strategy, // default: 'absolute'
context,
middlewareData: { arrow: { x: arrowX = 0, y: arrowY = 0 } = {} }, // data for arrow positioning
} = useFloating({
open: showTooltip, // state value binding
onOpenChange: setShowTooltip, // state value setter
middleware: [
offset(5), // offset from container border
autoPlacement({ padding: 5 }), // auto place vertically
shift({ padding: 5 }), // flip horizontally if necessary
arrow({ element: arrowRef, padding: 4 }), // add arrow element
inline(), // correct position for multiline anchor tags
],
});
// floating-ui hook
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, { delay: 100 }),
useFocus(context),
useRole(context, { role: "tooltip" }),
useDismiss(context, { ancestorScroll: true }),
]);
const triggerElementProps = getReferenceProps({ ...props, ref: reference });
const tooltipProps = getFloatingProps({
ref: floating,
style: {
position: strategy,
left: x ?? "",
top: y ?? "",
},
});
const arrowPlacement = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
}[placement.split("-")[0]];
// get tooltip data
let image: string;
let PageContent;
const filePath = props.href.slice(1); // remove slash from the beginning
const page = data.find((p) => p._raw.flattenedPath === filePath);
if (page && page.body.code.length > 0) {
const Component = usehook(page.body.code);
PageContent = Component;
image = page.image ?? "";
}
const fetchTooltipContent = () => {
setTooltipContentLoaded(false);
let Body: React.ReactElement = <Fragment />;
// strip out all other elements from tooltip content
// since we only need the paragraph
const elems = ["h1", "h2", "h3", "div", "img", "pre", "blockquote"].reduce(
(acc, elem) => ({ ...acc, [elem]: () => <Fragment /> }),
{}
);
if (PageContent) {
Body = (
<PageContent
components={{
...elems,
p: (props) => <Fragment {...props} />, // avoid hydration errors
wrapper: (props) => <div className="line-clamp-3" {...props} />,
}}
/>
);
setTooltipData({
content: Body,
image: image,
});
setTooltipContentLoaded(true);
}
};
useEffect(() => {
if (showTooltip) {
fetchTooltipContent();
}
}, [showTooltip]);
return (
<Fragment>
{render?.(triggerElementProps)}
<FloatingPortal>
<AnimatePresence>
{showTooltip && tooltipContentLoaded && (
<motion.div
{...tooltipProps}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", damping: 20, stiffness: 300 }}
>
<div
className="tooltip-box flex items-center space-x-2"
style={tooltipBoxStyle(theme)}
>
{tooltipData.image && (
<img
src={tooltipData.image}
alt=""
width={100}
height={100}
/>
)}
{tooltipData.content && (
<div className="tooltip-body" style={tooltipBodyStyle(theme)}>
{tooltipData.content}
</div>
)}
</div>
<div
ref={arrowRef}
className="tooltip-arrow"
style={tooltipArrowStyle({
theme,
x: arrowX,
y: arrowY,
side: arrowPlacement,
})}
></div>
</motion.div>
)}
</AnimatePresence>
</FloatingPortal>
</Fragment>
);
};

View File

@@ -0,0 +1 @@
export { Tooltip } from "./Tooltip";

View File

@@ -0,0 +1,6 @@
/* eslint import/no-default-export: off */
export const UnstyledLayout: React.FC<React.PropsWithChildren> = ({
children,
}) => {
return <div className="unstyled dark:text-white">{children}</div>;
};

View File

@@ -0,0 +1 @@
export { UnstyledLayout } from "./Unstyled";

View File

@@ -0,0 +1,22 @@
export { ThemeProvider } from "next-themes";
export { Nav, NavConfig, ThemeConfig } from "./Nav";
export { SearchProvider } from "./Search";
export {
Layout,
TableOfContents,
TocSection,
EditThisPage,
useTableOfContents,
} from "./Layout";
export { Pre } from "./Pre";
export { CustomLink } from "./Base/CustomLink";
export { BlogsList } from "./Blog";
export { SimpleLayout } from "./SimpleLayout";
export { DocsLayout } from "./DocsLayout";
export { UnstyledLayout } from "./UnstyledLayout";
export { BlogLayout } from "./BlogLayout";
export { Mermaid } from "./Mermaid";
export { SiteToc, NavItem, NavGroup } from "./SiteToc";
export { Comments, CommentsConfig } from "./Comments";
export { AuthorConfig } from "./types";

View File

@@ -0,0 +1,57 @@
// shared types used in more than one component
// TODO find out what's the best place to put them, what's the best practice
// layout
export interface NavLink {
name: string;
href: string;
}
export interface AuthorConfig {
name: string;
url: string;
logo: string;
}
// social
export type SocialPlatform = "github" | "discord";
export interface SocialLink {
label: SocialPlatform;
href: string;
}
// search
export type SearchProvider = "algolia" | "kbar";
export interface SearchProviderConfig {
provider: SearchProvider;
config: object;
}
// TEMP contentlayer
interface SharedFields {
title?: string;
description?: string;
image?: string;
layout: string;
showEditLink?: boolean;
showToc?: boolean;
showComments?: boolean;
isDraft?: boolean;
data: Array<string>;
}
interface ComputedFields {
urlPath: string;
editUrl?: string;
date?: string;
}
export interface Page extends SharedFields, ComputedFields {}
export interface Blog extends SharedFields, ComputedFields {
date: string; // TODO type?
authors?: Array<string>;
tags?: Array<string>;
}

View File

@@ -0,0 +1,60 @@
// ToC: get the html nodelist for headings
import { TocSection } from "../ui/Layout";
export function collectHeadings(nodes: NodeListOf<HTMLHeadingElement>) {
const sections: Array<TocSection> = [];
Array.from(nodes).forEach((node) => {
const { id, innerText: title, tagName: level } = node;
if (!(id && title)) {
return;
}
if (level === "H1") {
sections.push({ id, title, level, children: [] });
}
const parentSection = sections[sections.length - 1];
if (level === "H2") {
if (parentSection && level > parentSection.level) {
(parentSection as TocSection).children.push({
id,
title,
level,
children: [],
});
} else {
sections.push({ id, title, level, children: [] });
}
}
if (level === "H3") {
const subSection =
parentSection?.children[parentSection?.children?.length - 1];
if (subSection && level > subSection.level) {
(subSection as TocSection).children.push({
id,
title,
level,
children: [],
});
} else if (parentSection && level > parentSection.level) {
(parentSection as TocSection).children.push({
id,
title,
level,
children: [],
});
} else {
sections.push({ id, title, level, children: [] });
}
}
// TODO types
sections.push(...collectHeadings((node.children as any) ?? []));
});
return sections;
}

View File

@@ -0,0 +1,8 @@
export const formatDate = (date: string, locales = "en-US") => {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
return new Date(date).toLocaleDateString(locales, options);
};

View File

@@ -0,0 +1,25 @@
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = ({
url,
analyticsID,
}: {
url: string;
analyticsID: string;
}) => {
if (typeof window.gtag !== undefined) {
window.gtag("config", analyticsID, {
page_path: url,
});
}
};
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }) => {
if (typeof window.gtag !== undefined) {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
}
};

View File

@@ -0,0 +1,2 @@
export { pageview } from "./gtag";
export { collectHeadings } from "./collectHeadings";

View File

@@ -0,0 +1,4 @@
export const nameFromUrl = (url: string) => {
const name = url.split("/").slice(-1)[0].replace("-", " ");
return name.charAt(0).toUpperCase() + name.slice(1);
};

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "es2020",
"moduleResolution": "node",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,4 @@
extension: [ts]
node-option:
- experimental-specifier-resolution=node
- loader=ts-node/esm

View File

@@ -0,0 +1,25 @@
# @portaljs/remark-callouts
## 1.0.4
### Patch Changes
- [`affca05`](https://github.com/datopian/portaljs/commit/affca0505851a87a4295045b60a78926d34eef78) Thanks [@olayway](https://github.com/olayway)! - Fix: incorrect path to styles.css in package.json exports field.
## 1.0.3
### Patch Changes
- [`e82e2ae`](https://github.com/datopian/portaljs/commit/e82e2ae0211ea3e4701703d353b44cf1001434ef) Thanks [@olayway](https://github.com/olayway)! - Fix: replace deprecated `prepublish` script with `prepare`
## 1.0.2
### Patch Changes
- [`40d80d2`](https://github.com/datopian/portaljs/commit/40d80d2282bf8464c1aafb393975065078ad9ea3) Thanks [@olayway](https://github.com/olayway)! - Fix: missing files in the published package.
## 1.0.1
### Patch Changes
- [`b7158a5`](https://github.com/datopian/portaljs/commit/b7158a5be668018d9b947f9c9d63fa30fa91d18b) Thanks [@olayway](https://github.com/olayway)! - Fix what's getting published to npm.

View File

@@ -0,0 +1,121 @@
# @portaljs/remark-callouts
Remark plugin to add support for blockquote-based callouts/admonitions similar to the approach of [Obsidian](https://help.obsidian.md/How+to/Use+callouts) and [Microsoft Learn](https://learn.microsoft.com/en-us/contribute/markdown-reference#alerts-note-tip-important-caution-warning) style.
Using this plugin, markdown like this:
```md
> [!tip]
> hello callout
```
Would render as a callout like this:
<img width="645" alt="Tip callout block" src="https://user-images.githubusercontent.com/42637597/193016397-49a90b44-cf3d-4eeb-9ad6-c0c1e374ed27.png">
## Features supported
- [x] Supports blockquote style callouts
- [x] Supports nested blockquote callouts
- [x] Supports 13 types out of the box (with appropriate styling in default theme) - see list below
- [x] Supports aliases for types
- [x] Defaults to note callout for all other types eg. `> [!xyz]`
- [x] Supports dark and light mode styles
Future support:
- [ ] Support custom types and icons
- [ ] Support custom aliases
- [ ] Support Foldable callouts
- [ ] Support custom styles
## Geting Started
### Installation
```bash
npm install remark-callouts
```
### Usage
```js
import callouts from "remark-callouts";
await remark()
.use(remarkParse)
.use(callouts)
.use(remarkRehype)
.use(rehypeStringify).process(`\
> [!tip]
> hello callout
`);
```
HTML output
```js
<div>
<blockquote class="callout">
<div class="callout-title tip">
<span class="callout-icon">
<svg>...</svg>
</span>
<strong>Tip</strong>
</div>
<div class="callout-content">
<p>hello callout</p>
</div>
</blockquote>
</div>
```
Import the styles in your .css file
```css
@import "remark-callouts/styles.css";
```
or in your app.js
```js
import "remark-callouts/styles.css";
```
### Supported Callout Types
- note
- tip `aliases: hint, important`
- warning `alises: caution, attention`
- abstract `aliases: summary, tldr`
- info
- todo
- success `aliases: check, done`
- question `aliases: help, faq`
- failure `aliases: fail, missing`
- danger `alias: error`
- bug
- example
- quote `alias: cite`
# Change Log
## [2.0.0] - 2022-11-21
### Added
- Classname for icon.
### Changed
- Extract css styles which can be imported separately.
## [1.0.2] - 2022-11-03
### Fixed
- Case insensitive match for types.
## License
MIT

View File

@@ -0,0 +1,47 @@
{
"name": "@portaljs/remark-callouts",
"version": "1.0.4",
"description": "Remark plugin to add support for blockquote-based admonitions/callouts",
"repository": {
"type": "git",
"url": "git+https://github.com/datopian/portaljs.git",
"directory": "packages/remark-callouts"
},
"keywords": [
"remark",
"remark-plugin",
"markdown",
"admonitions",
"callouts",
"obsidian"
],
"author": "Rufus Pollock",
"license": "MIT",
"bugs": {
"url": "https://github.com/datopian/portaljs/issues"
},
"homepage": "https://github.com/datopian/portaljs#readme",
"publishConfig": {
"access": "public"
},
"scripts": {
"prepare": "nx build remark-callouts"
},
"files": [
"dist"
],
"dependencies": {
"mdast-util-from-markdown": "^1.2.0",
"svg-parser": "^2.0.4",
"unist-util-visit": "^4.1.0"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./styles.css": "./dist/styles.css"
}
}

View File

@@ -0,0 +1,42 @@
{
"name": "remark-callouts",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/remark-callouts/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/remark-callouts/**/*.ts"]
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "TS_NODE_PROJECT='packages/remark-callouts/tsconfig.spec.json' mocha --config packages/remark-callouts/.mocharc.yaml packages/remark-callouts/test/**"
}
},
"build": {
"executor": "@nrwl/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"entryFile": "packages/remark-callouts/src/index.ts",
"outputPath": "packages/remark-callouts/dist",
"compiler": "babel",
"tsConfig": "packages/remark-callouts/tsconfig.lib.json",
"project": "packages/remark-callouts/package.json",
"format": ["esm", "cjs"],
"extractCss": true,
"assets": [
{
"glob": "packages/remark-callouts/styles.css",
"input": ".",
"output": "."
}
]
}
}
},
"tags": []
}

View File

@@ -0,0 +1,2 @@
export * from "./lib/remark-callouts";
export { default } from "./lib/remark-callouts";

Some files were not shown because too many files have changed in this diff Show More