[site][xl]: move site folder to root
This commit is contained in:
16
site/components/CustomLink.js
Normal file
16
site/components/CustomLink.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function CustomLink({ as, href, ...otherProps }) {
|
||||
return (
|
||||
<>
|
||||
<Link legacyBehavior as={as} href={href}>
|
||||
<a {...otherProps} />
|
||||
</Link>
|
||||
<style jsx>{`
|
||||
a {
|
||||
color: tomato;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
186
site/components/ExcelViewerApp.js
Normal file
186
site/components/ExcelViewerApp.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import XLSX from 'xlsx';
|
||||
import React from 'react';
|
||||
|
||||
function SheetJSApp() {
|
||||
const [data, setData] = React.useState([]);
|
||||
const [cols, setCols] = React.useState([]);
|
||||
|
||||
const handleFile = (file) => {
|
||||
const reader = new FileReader();
|
||||
const rABS = !!reader.readAsBinaryString;
|
||||
reader.onload = (e) => {
|
||||
/* Parse data */
|
||||
const bstr = e.target.result;
|
||||
const wb = XLSX.read(bstr, {type:rABS ? 'binary' : 'array'});
|
||||
displayWorkbook(wb);
|
||||
};
|
||||
if(rABS) reader.readAsBinaryString(file); else reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
const handleUrl = (url) => {
|
||||
let oReq = new XMLHttpRequest();
|
||||
oReq.open("GET", url, true);
|
||||
oReq.responseType = "arraybuffer";
|
||||
oReq.onload = function (e) {
|
||||
let arraybuffer = oReq.response;
|
||||
/* not responseText!! */
|
||||
|
||||
/* convert data to binary string */
|
||||
let data = new Uint8Array(arraybuffer);
|
||||
let arr = new Array();
|
||||
for (let i = 0; i != data.length; ++i) arr[i] = String.fromCharCode(data[i]);
|
||||
let bstr = arr.join("");
|
||||
/* Call XLSX */
|
||||
let workbook = XLSX.read(bstr, {type: "binary"});
|
||||
displayWorkbook(workbook);
|
||||
};
|
||||
|
||||
oReq.send();
|
||||
}
|
||||
|
||||
const displayWorkbook = (wb) => {
|
||||
/* Get first worksheet */
|
||||
const wsname = wb.SheetNames[0];
|
||||
const ws = wb.Sheets[wsname];
|
||||
/t Convert array of arrays */
|
||||
const data = XLSX.utils.sheet_to_json(ws, {header:1});
|
||||
/* Update state */
|
||||
setData(data);
|
||||
setCols(make_cols(ws['!ref']));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DragDropFile handleFile={handleFile}>
|
||||
<h2>Drag or choose a spreadsheet file</h2>
|
||||
<div className="">
|
||||
<DataInput handleFile={handleFile} />
|
||||
</div>
|
||||
</DragDropFile>
|
||||
<div className="mb-6">
|
||||
<h3>Enter spreadsheet URL</h3>
|
||||
<UrlInput handleUrl={handleUrl} />
|
||||
</div>
|
||||
<div className="row">
|
||||
<OutTable data={data} cols={cols} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(typeof module !== 'undefined') module.exports = SheetJSApp
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
Simple HTML5 file drag-and-drop wrapper
|
||||
usage: <DragDropFile handleFile={handleFile}>...</DragDropFile>
|
||||
handleFile(file:File):void;
|
||||
*/
|
||||
|
||||
function DragDropFile({ handleFile, children }) {
|
||||
const suppress = (e) => { e.stopPropagation(); e.preventDefault(); };
|
||||
const handleDrop = (e) => { e.stopPropagation(); e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
if(files && files[0]) handleFile(files[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={suppress}
|
||||
onDragOver={suppress}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UrlInput({ handleUrl }) {
|
||||
const handleChange = (e) => {
|
||||
const url = e.target.value;
|
||||
if(url) handleUrl(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="form-inline">
|
||||
<div className="form-group">
|
||||
<label htmlFor="url">Spreadsheet URL (with CORS enabled!)</label>
|
||||
<br />
|
||||
<small>Here is one: http://localhost:3000/_files/eight-centuries-of-global-real-interest-rates-r-g-and-the-suprasecular-decline-1311-2018-data.xlsx</small>
|
||||
<br />
|
||||
<input
|
||||
type="text"
|
||||
id="url"
|
||||
className="border w-96"
|
||||
accept={SheetJSFT}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Simple HTML5 file input wrapper
|
||||
usage: <DataInput handleFile={callback} />
|
||||
handleFile(file:File):void;
|
||||
*/
|
||||
|
||||
function DataInput({ handleFile }) {
|
||||
const handleChange = (e) => {
|
||||
const files = e.target.files;
|
||||
if(files && files[0]) handleFile(files[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="form-inline">
|
||||
<div className="form-group">
|
||||
<label htmlFor="file">Select spreadsheet file</label>
|
||||
<br />
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
id="file"
|
||||
accept={SheetJSFT}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Simple HTML Table
|
||||
usage: <OutTable data={data} cols={cols} />
|
||||
data:Array<Array<any> >;
|
||||
cols:Array<{name:string, key:number|string}>;
|
||||
*/
|
||||
function OutTable({ data, cols }) {
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>{cols.map((c) => <th key={c.key}>{c.name}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((r,i) => <tr key={i}>
|
||||
{cols.map(c => <td key={c.key}>{ r[c.key] }</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* list of supported file types */
|
||||
const SheetJSFT = [
|
||||
"xlsx", "xlsb", "xlsm", "xls", "xml", "csv", "txt", "ods", "fods", "uos", "sylk", "dif", "dbf", "prn", "qpw", "123", "wb*", "wq*", "html", "htm"
|
||||
].map(x => `.${x}`).join(",");
|
||||
|
||||
/* generate an array of column objects */
|
||||
const make_cols = refstr => {
|
||||
let o = [], C = XLSX.utils.decode_range(refstr).e.c + 1;
|
||||
for(var i = 0; i < C; ++i) o[i] = {name:XLSX.utils.encode_col(i), key:i}
|
||||
return o;
|
||||
};
|
||||
34
site/components/Layout.tsx
Normal file
34
site/components/Layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
import Nav from "./Nav";
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
children;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{title && <NextSeo title={title} />}
|
||||
<Nav />
|
||||
<div className="mx-auto p-6">{children}</div>
|
||||
<footer className="flex items-center justify-center w-full h-24 border-t">
|
||||
<a
|
||||
className="flex items-center justify-center"
|
||||
href="https://datopian.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Built by{" "}
|
||||
<img
|
||||
src="/datopian-logo.png"
|
||||
alt="Datopian Logo"
|
||||
className="h-6 ml-2"
|
||||
/>
|
||||
</a>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
site/components/MDXPage.tsx
Normal file
40
site/components/MDXPage.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import layouts from "layouts";
|
||||
|
||||
export default function DRD({ source, frontMatter }) {
|
||||
const Layout = ({ children }) => {
|
||||
if (frontMatter.layout) {
|
||||
let LayoutComponent = layouts[frontMatter.layout];
|
||||
return <LayoutComponent {...frontMatter}>{children}</LayoutComponent>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prose mx-auto">
|
||||
<header>
|
||||
<div className="mb-6">
|
||||
{/* Default layout */}
|
||||
{!frontMatter.layout && (
|
||||
<>
|
||||
<h1>{frontMatter.title}</h1>
|
||||
{frontMatter.author && (
|
||||
<div className="-mt-6">
|
||||
<p className="opacity-60 pl-1">{frontMatter.author}</p>
|
||||
</div>
|
||||
)}
|
||||
{frontMatter.description && (
|
||||
<p className="description">{frontMatter.description}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<Layout>
|
||||
<MDXRemote {...source} />
|
||||
</Layout>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
site/components/Nav.js
Normal file
96
site/components/Nav.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Fragment } from "react";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { BellIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
import { siteConfig } from "config/siteConfig";
|
||||
|
||||
import Link from "next/link";
|
||||
import GitHubButton from "react-next-github-btn";
|
||||
|
||||
const navigation = siteConfig.navLinks;
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function Nav() {
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-gray-800">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex items-center justify-between h-16">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button*/}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Link href="/" className="text-white">
|
||||
Portal.JS
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:ml-6">
|
||||
<div className="flex space-x-4">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
href={item.href}
|
||||
key={item.name}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-gray-900 text-white"
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-white",
|
||||
"px-3 py-2 rounded-md text-sm font-medium"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 justify-end">
|
||||
<GitHubButton
|
||||
href="https://github.com/datopian/portal.js"
|
||||
data-color-scheme="no-preference: light; light: light; dark: dark;"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star datopian/portal.js on GitHub"
|
||||
>
|
||||
Stars
|
||||
</GitHubButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-gray-900 text-white"
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-white",
|
||||
"block px-3 py-2 rounded-md text-base font-medium"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
46
site/components/data-literate/DataLiterate.js
Normal file
46
site/components/data-literate/DataLiterate.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Layout from '../Layout'
|
||||
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Head from 'next/head'
|
||||
|
||||
import CustomLink from '../CustomLink'
|
||||
import { Vega, VegaLite } from 'react-vega'
|
||||
|
||||
// Custom components/renderers to pass to MDX.
|
||||
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||
// to handle import statements. Instead, you must include components in scope
|
||||
// here.
|
||||
const components = {
|
||||
a: CustomLink,
|
||||
Table: dynamic(() => import('./Table')),
|
||||
Excel: dynamic(() => import('./Excel')),
|
||||
// TODO: try and make these dynamic ...
|
||||
Vega: Vega,
|
||||
VegaLite: VegaLite,
|
||||
LineChart: dynamic(() => import('./LineChart')),
|
||||
Head,
|
||||
}
|
||||
|
||||
export default function DataLiterate({ source, frontMatter }) {
|
||||
return (
|
||||
<Layout title={frontMatter.title}>
|
||||
<div className="prose mx-auto">
|
||||
<header>
|
||||
<div className="mb-6">
|
||||
<h1>{frontMatter.title}</h1>
|
||||
{frontMatter.author && (
|
||||
<div className="-mt-6"><p className="opacity-60 pl-1">{frontMatter.author}</p></div>
|
||||
)}
|
||||
{frontMatter.description && (
|
||||
<p className="description">{frontMatter.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
74
site/components/data-literate/Excel.js
Normal file
74
site/components/data-literate/Excel.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import axios from 'axios'
|
||||
import XLSX from 'xlsx'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import Table from './Table'
|
||||
|
||||
export default function Excel ({ src='' }) {
|
||||
const [data, setData] = React.useState([])
|
||||
const [cols, setCols] = React.useState([])
|
||||
const [workbook, setWorkbook] = React.useState(null)
|
||||
const [error, setError] = React.useState('')
|
||||
const [hasMounted, setHasMounted] = React.useState(0)
|
||||
|
||||
// so this is here so we re-render this in the browser
|
||||
// and not just when we build the page statically in nextjs
|
||||
useEffect(() => {
|
||||
if (hasMounted==0) {
|
||||
handleUrl(src)
|
||||
}
|
||||
setHasMounted(1)
|
||||
})
|
||||
|
||||
function handleUrl(url) {
|
||||
// if url is external may have CORS issue so we proxy it ...
|
||||
if (url.startsWith('http')) {
|
||||
const PROXY_URL = window.location.origin + '/api/proxy'
|
||||
url = PROXY_URL + '?url=' + encodeURIComponent(url)
|
||||
}
|
||||
axios.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
}).then((res) => {
|
||||
let out = new Uint8Array(res.data)
|
||||
let workbook = XLSX.read(out, {type: "array"})
|
||||
// Get first worksheet
|
||||
const wsname = workbook.SheetNames[0]
|
||||
const ws = workbook.Sheets[wsname]
|
||||
// Convert array of arrays
|
||||
const datatmp = XLSX.utils.sheet_to_json(ws, {header:1})
|
||||
const colstmp = make_cols(ws['!ref'])
|
||||
setData(datatmp)
|
||||
setCols(colstmp)
|
||||
setWorkbook(workbook)
|
||||
}).catch((e) => {
|
||||
setError(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error &&
|
||||
<div>
|
||||
There was an error loading the excel file at {src}:
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
}
|
||||
{workbook &&
|
||||
<ul>
|
||||
{workbook.SheetNames.map((value, index) => {
|
||||
return <li key={index}>{value}</li>
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
<Table data={data} cols={cols} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* generate an array of column objects */
|
||||
const make_cols = refstr => {
|
||||
let o = [], C = XLSX.utils.decode_range(refstr).e.c + 1
|
||||
for(var i = 0; i < C; ++i) o[i] = {name:XLSX.utils.encode_col(i), key:i}
|
||||
return o
|
||||
}
|
||||
|
||||
33
site/components/data-literate/LineChart.js
Normal file
33
site/components/data-literate/LineChart.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Vega, VegaLite } from 'react-vega'
|
||||
|
||||
export default function LineChart( { data=[] }) {
|
||||
var tmp = data
|
||||
if (Array.isArray(data)) {
|
||||
tmp = data.map((r,i) => {
|
||||
return { x: r[0], y: r[1] }
|
||||
})
|
||||
}
|
||||
const vegaData = { "table": tmp }
|
||||
const spec = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"mark": "line",
|
||||
"data": {
|
||||
"name": "table"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "x",
|
||||
"timeUnit": "year",
|
||||
"type": "temporal"
|
||||
},
|
||||
"y": {
|
||||
"field": "y",
|
||||
"type": "quantitative"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VegaLite data={ vegaData } spec={ spec } />
|
||||
)
|
||||
}
|
||||
83
site/components/data-literate/Table.js
Normal file
83
site/components/data-literate/Table.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import axios from 'axios'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const papa = require("papaparse")
|
||||
|
||||
/*
|
||||
Simple HTML Table
|
||||
usage: <OutTable data={data} cols={cols} />
|
||||
data:Array<Array<any> >;
|
||||
cols:Array<{name:string, key:number|string}>;
|
||||
*/
|
||||
export default function Table({ data=[], cols=[], csv='', url='' }) {
|
||||
if (csv) {
|
||||
const out = parseCsv(csv)
|
||||
data = out.rows
|
||||
cols = out.cols
|
||||
}
|
||||
|
||||
const [ourdata, setData] = React.useState(data)
|
||||
const [ourcols, setCols] = React.useState(cols)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
function loadUrl(path) {
|
||||
// HACK: duplicate of Excel code - maybe refactor
|
||||
// if url is external may have CORS issue so we proxy it ...
|
||||
if (url.startsWith('http')) {
|
||||
const PROXY_URL = window.location.origin + '/api/proxy'
|
||||
url = PROXY_URL + '?url=' + encodeURIComponent(url)
|
||||
}
|
||||
axios.get(url).then((res) => {
|
||||
const { rows, fields } = parseCsv(res.data)
|
||||
setData(rows)
|
||||
setCols(fields)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleTable data={ourdata} cols={ourcols} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Simple HTML Table
|
||||
usage: <OutTable data={data} cols={cols} />
|
||||
data:Array<Array<any> >;
|
||||
cols:Array<{name:string, key:number|string}>;
|
||||
*/
|
||||
function SimpleTable({ data=[], cols=[] }) {
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>{cols.map((c) => <th key={c.key}>{c.name}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((r,i) => <tr key={i}>
|
||||
{cols.map(c => <td key={c.key}>{ r[c.key] }</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function parseCsv(csv) {
|
||||
csv = csv.trim()
|
||||
const rawdata = papa.parse(csv, {header: true})
|
||||
const cols = rawdata.meta.fields.map((r,i) => {
|
||||
return { key: r, name: r }
|
||||
})
|
||||
return {
|
||||
rows: rawdata.data,
|
||||
fields: cols
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user