[#754,landing page][xl]: added hero section and feature section, enabled dark mode, updated navbar, enabled toc
This commit is contained in:
15
site/components/BaseLink.tsx
Normal file
15
site/components/BaseLink.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Link from "next/link";
|
||||
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 default BaseLink;
|
||||
68
site/components/Features.tsx
Normal file
68
site/components/Features.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
const features: { title: string; description: string; icon: string }[] = [
|
||||
{
|
||||
title: 'Unified sites',
|
||||
description:
|
||||
'Present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. wordpress) with a common internal API',
|
||||
icon: '/images/icon-unified-sites.svg',
|
||||
},
|
||||
{
|
||||
title: 'Developer friendly',
|
||||
description: 'Built with familiar frontend tech Javascript, React etc',
|
||||
icon: '/images/icon-dev-friendly.svg',
|
||||
},
|
||||
{
|
||||
title: 'Batteries included',
|
||||
description:
|
||||
'Full set of portal components out of the box e.g. catalog search, dataset showcase, blog etc.',
|
||||
icon: '/images/icon-batteries-included.svg',
|
||||
},
|
||||
{
|
||||
title: 'Easy to theme and customize',
|
||||
description:
|
||||
'installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.',
|
||||
icon: '/images/icon-easy-to-theme.svg',
|
||||
},
|
||||
{
|
||||
title: 'Extensible',
|
||||
description: 'quickly extend and develop/import your own React components',
|
||||
icon: '/images/icon-extensible.svg',
|
||||
},
|
||||
{
|
||||
title: 'Well documented',
|
||||
description:
|
||||
'full set of documentation plus the documentation of NextJS and Apollo.',
|
||||
icon: '/images/icon-well-documented.svg',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Features() {
|
||||
return (
|
||||
<div className="lg:max-w-8xl mx-auto px-4 lg:px-8 xl:px-12">
|
||||
<h2 className="text-3xl font-bold">How Portal.JS works?</h2>
|
||||
<p className="text-lg mt-8">
|
||||
Portal.JS is built in JavaScript and React on top of the popular Next.js
|
||||
framework, assuming a "decoupled" approach where the frontend is a
|
||||
separate service from the backend and interacts with backend(s) via an
|
||||
API. It can be used with any backend and has out of the box support for
|
||||
CKAN.
|
||||
</p>
|
||||
<div className="not-prose my-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<div className="group relative rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,theme(colors.sky.50)),var(--quick-links-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.blue.300),theme(colors.blue.400),theme(colors.blue.500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:theme(colors.slate.800)]" />
|
||||
<div className="relative overflow-hidden rounded-xl p-6">
|
||||
<img src={feature.icon} alt="" className="h-24 w-auto" />
|
||||
<h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
|
||||
<span className="absolute -inset-px rounded-xl" />
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-slate-700 dark:text-slate-400">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
195
site/components/Hero.tsx
Normal file
195
site/components/Hero.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import clsx from 'clsx';
|
||||
import Highlight, { defaultProps } from 'prism-react-renderer';
|
||||
import { Fragment, useRef } from 'react';
|
||||
|
||||
const codeLanguage = 'javascript';
|
||||
const code = `export default {
|
||||
strategy: 'predictive',
|
||||
engine: {
|
||||
cpus: 12,
|
||||
backups: ['./storage/cache.wtf'],
|
||||
},
|
||||
}`;
|
||||
|
||||
const tabs = [
|
||||
{ name: 'cache-advance.config.js', isActive: true },
|
||||
{ name: 'package.json', isActive: false },
|
||||
];
|
||||
|
||||
function TrafficLightsIcon(props) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
|
||||
<circle cx="5" cy="5" r="4.5" />
|
||||
<circle cx="21" cy="5" r="4.5" />
|
||||
<circle cx="37" cy="5" r="4.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint jsx-a11y/label-has-associated-control: off */
|
||||
export function Hero() {
|
||||
const el = useRef(null);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden -mb-32 mt-[-4.5rem] pb-32 pt-[4.5rem] lg:mt-[-4.75rem] lg:pt-[4.75rem]">
|
||||
<div className="py-16 sm:px-2 lg:relative lg:py-20 lg:px-0">
|
||||
{/* Commented code on line 37, 39 and 113 will reenable the two columns hero */}
|
||||
{/* <div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12"> */}
|
||||
<div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-4xl lg:grid-cols-1 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||
{/* <div className="relative mb-10 lg:mb-0 md:text-center lg:text-left"> */}
|
||||
<div className="relative mb-10 lg:mb-0 md:text-center lg:text-center">
|
||||
<div role="heading">
|
||||
<h1 className="inline bg-gradient-to-r from-blue-500 via-blue-300 to-blue-500 bg-clip-text text-5xl tracking-tight text-transparent">
|
||||
The JavaScript framework for data portals
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mt-4 text-xl tracking-tight text-slate-400">
|
||||
Portal.JS is a framework for rapidly building rich data portal
|
||||
frontends using a modern frontend approach. It can be used to
|
||||
present a single dataset or build a full-scale data
|
||||
catalog/portal.
|
||||
</p>
|
||||
<div className="mt-8 sm:mx-auto sm:text-center lg:text-left lg:mx-0">
|
||||
<p className="text-base font-medium text-slate-400 dark:text-slate-400">
|
||||
Sign up to get notified about updates
|
||||
</p>
|
||||
<form
|
||||
method="POST"
|
||||
name="get-updates"
|
||||
data-netlify="true"
|
||||
action="/subscribed"
|
||||
className="mt-3 sm:flex"
|
||||
>
|
||||
<label htmlFor="name" className="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Your name"
|
||||
className="block w-full sm:flex-auto sm:w-32 px-2 py-3 text-base rounded-md bg-slate-200 dark:bg-slate-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 focus:ring-offset-gray-900"
|
||||
/>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Your email"
|
||||
className="block w-full mt-3 sm:flex-auto sm:w-64 sm:mt-0 sm:ml-3 px-2 py-3 text-base rounded-md bg-slate-200 dark:bg-slate-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 focus:ring-offset-gray-900"
|
||||
/>
|
||||
<input type="hidden" name="form-name" value="get-updates" />
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-none mt-3 px-6 py-3 border border-transparent text-base font-medium rounded-md text-slate-900 bg-blue-400 hover:bg-blue-300 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50 active:bg-sky-500 sm:mt-0 sm:ml-3"
|
||||
>
|
||||
Notify me
|
||||
</button>
|
||||
</form>
|
||||
{/* <p className="mt-3 text-sm text-slate-400 dark:text-slate-300 sm:mt-4">
|
||||
We are actively trialling and developing Flowershow. If you'd
|
||||
like to get notified about our progress and important updates,
|
||||
please sign up.
|
||||
</p> */}
|
||||
</div>
|
||||
<p className="my-10 text-l tracking-wide">
|
||||
<span>A project of</span>
|
||||
<a
|
||||
href="https://www.datopian.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
src="/images/datopian_logo.png"
|
||||
alt="Datopian"
|
||||
className="mx-2 mb-1 h-6 inline bg-black rounded-full"
|
||||
/>
|
||||
<span>Datopian</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/* <div className="relative">
|
||||
<div className="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur">
|
||||
<div className="absolute -top-px left-20 right-11 h-px bg-gradient-to-r from-sky-300/0 via-sky-300/70 to-sky-300/0" />
|
||||
<div className="absolute -bottom-px left-11 right-20 h-px bg-gradient-to-r from-blue-400/0 via-blue-400 to-blue-400/0" />
|
||||
<div className="pl-4 pt-4">
|
||||
<TrafficLightsIcon className="h-2.5 w-auto stroke-slate-500/30" />
|
||||
<div className="mt-4 flex space-x-2 text-xs">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.name}
|
||||
className={clsx(
|
||||
'flex h-6 rounded-full',
|
||||
tab.isActive
|
||||
? 'bg-gradient-to-r from-sky-400/30 via-sky-400 to-sky-400/30 p-px font-medium text-sky-300'
|
||||
: 'text-slate-500'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center rounded-full px-2.5',
|
||||
tab.isActive && 'bg-slate-800'
|
||||
)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex items-start px-1 text-sm">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="select-none border-r border-slate-300/5 pr-4 font-mono text-slate-600"
|
||||
>
|
||||
{Array.from({
|
||||
length: code.split('\n').length,
|
||||
}).map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
{(index + 1).toString().padStart(2, '0')}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
code={code}
|
||||
language={codeLanguage}
|
||||
theme={undefined}
|
||||
>
|
||||
{({
|
||||
className,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<pre
|
||||
className={clsx(className, 'flex overflow-x-auto pb-6')}
|
||||
style={style}
|
||||
>
|
||||
<code className="px-4">
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<div key={lineIndex} {...getLineProps({ line })}>
|
||||
{line.map((token, tokenIndex) => (
|
||||
<span
|
||||
key={tokenIndex}
|
||||
{...getTokenProps({ token })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,91 @@
|
||||
import { NextSeo } from "next-seo";
|
||||
import { siteConfig } from '@/config/siteConfig';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import Nav from "./Nav";
|
||||
import Nav from './Nav';
|
||||
|
||||
function useTableOfContents(tableOfContents) {
|
||||
const [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id);
|
||||
|
||||
const getHeadings = useCallback((toc) => {
|
||||
return toc
|
||||
.flatMap((node) => [node.id, ...node.children.map((child) => child.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;
|
||||
}
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
title,
|
||||
tableOfContents = [],
|
||||
}: {
|
||||
children;
|
||||
title?: string;
|
||||
tableOfContents?;
|
||||
}) {
|
||||
const { toc } = children.props;
|
||||
|
||||
const currentSection = useTableOfContents(tableOfContents);
|
||||
|
||||
function isActive(section) {
|
||||
if (section.id === currentSection) {
|
||||
return true;
|
||||
}
|
||||
if (!section.children) {
|
||||
return false;
|
||||
}
|
||||
return section.children.findIndex(isActive) > -1;
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="mx-auto p-6 bg-background dark:bg-background-dark">
|
||||
{children}
|
||||
</div>
|
||||
<footer className="flex items-center justify-center w-full h-24 border-t dark:border-slate-900 bg-background dark:bg-background-dark">
|
||||
<a
|
||||
className="flex items-center justify-center"
|
||||
href="https://datopian.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Built by{" "}
|
||||
Built by{' '}
|
||||
<img
|
||||
src="/datopian-logo.png"
|
||||
alt="Datopian Logo"
|
||||
@@ -29,6 +93,52 @@ export default function Layout({
|
||||
/>
|
||||
</a>
|
||||
</footer>
|
||||
{/** TABLE OF CONTENTS */}
|
||||
{tableOfContents.length > 0 && (toc ?? siteConfig.tableOfContents) && (
|
||||
<div className="hidden xl:fixed xl:right-0 xl:top-[4.5rem] xl:block xl:w-1/5 xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6 xl:mb-16">
|
||||
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||
<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={
|
||||
isActive(section)
|
||||
? 'text-sky-500'
|
||||
: '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={
|
||||
isActive(subSection)
|
||||
? 'text-sky-500'
|
||||
: 'hover:text-slate-600 dark:hover:text-slate-300'
|
||||
}
|
||||
>
|
||||
{subSection.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import layouts from "layouts";
|
||||
|
||||
export default function DRD({ source, frontMatter }) {
|
||||
export default function MDXPage({ source, frontMatter }) {
|
||||
const Layout = ({ children }) => {
|
||||
if (frontMatter.layout) {
|
||||
let LayoutComponent = layouts[frontMatter.layout];
|
||||
@@ -11,7 +11,7 @@ export default function DRD({ source, frontMatter }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prose mx-auto">
|
||||
<div className="prose mx-auto 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">
|
||||
<header>
|
||||
<div className="mb-6">
|
||||
{/* Default layout */}
|
||||
|
||||
165
site/components/MobileNavigation.tsx
Normal file
165
site/components/MobileNavigation.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
|
||||
import { siteConfig } from "../config/siteConfig";
|
||||
import BaseLink from "./BaseLink";
|
||||
// import { SearchContext, SearchField } from "./search/index.jsx";
|
||||
|
||||
// const Search = SearchContext(siteConfig.search?.provider);
|
||||
|
||||
function MenuIcon(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>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseIcon(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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MobileNavigation({ navigation }) {
|
||||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
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]);
|
||||
|
||||
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-slate-900/50 pr-10 backdrop-blur lg:hidden"
|
||||
aria-label="Navigation"
|
||||
>
|
||||
<Dialog.Panel className="relative min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 dark:bg-slate-900 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-slate-900 dark:text-white text-2xl ml-6">
|
||||
{siteConfig.title}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{/* {Search && (
|
||||
<Search>
|
||||
{({ query }) => <SearchField mobile onOpen={query.toggle} />}
|
||||
</Search>
|
||||
)} */}
|
||||
<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">
|
||||
{navigation.map((link) => (
|
||||
<Menu as="div" key={link.name} className="relative">
|
||||
<Menu.Button>
|
||||
{Object.prototype.hasOwnProperty.call(link, "href") ? (
|
||||
<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>
|
||||
) : (
|
||||
<li key={link.name}>
|
||||
<div className="flex 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 dark:hover:fill-slate-300 fill-slate-500 hover:fill-slate-600">
|
||||
{link.name}
|
||||
<svg
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7 10l5 5 5-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</Menu.Button>
|
||||
{Object.prototype.hasOwnProperty.call("subItems") && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="transform opacity-0 scale-5"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-5"
|
||||
>
|
||||
<Menu.Items className="flex flex-col ml-3">
|
||||
{link.subItems.map((subItem) => (
|
||||
<Menu.Item key={subItem.name}>
|
||||
<BaseLink
|
||||
href={subItem.href}
|
||||
className="text-slate-500 inline-flex items-center mt-2 px-1 pt-1 text-sm font-medium hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300"
|
||||
>
|
||||
{subItem.name}
|
||||
</BaseLink>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
)}
|
||||
</Menu>
|
||||
))}
|
||||
</ul>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
136
site/components/Nav.tsx
Normal file
136
site/components/Nav.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { siteConfig } from '../config/siteConfig';
|
||||
import MobileNavigation from './MobileNavigation';
|
||||
import NavItem from './NavItem';
|
||||
import ThemeSelector from './ThemeSelector';
|
||||
// import { SearchContext, SearchField } from "./search/index.jsx";
|
||||
|
||||
// const Search = SearchContext(siteConfig.search?.provider);
|
||||
|
||||
function GitHubIcon(props) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor" {...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>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscordIcon(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>
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarTitle() {
|
||||
const chunk = (
|
||||
<>
|
||||
{siteConfig.navbarTitle?.logo && (
|
||||
<img
|
||||
src={siteConfig.navbarTitle.logo}
|
||||
alt={siteConfig.navbarTitle.text}
|
||||
className="w-9 h-9 mr-1 fill-white"
|
||||
/>
|
||||
)}
|
||||
{siteConfig.navbarTitle?.text}
|
||||
{siteConfig.navbarTitle?.version && (
|
||||
<div className="mx-2 rounded-full border border-slate-500 py-1 px-3 text-xs text-slate-500">
|
||||
{siteConfig.navbarTitle?.version}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Home page"
|
||||
className="flex items-center font-extrabold text-xl sm:text-2xl text-slate-900 dark:text-white"
|
||||
>
|
||||
{siteConfig.navbarTitle && chunk}
|
||||
{!siteConfig.navbarTitle && siteConfig.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Nav() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [modifierKey, setModifierKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const applePlatform = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||
|
||||
setModifierKey(applePlatform ? '⌘' : 'Ctrl ');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setIsScrolled(window.scrollY > 0);
|
||||
}
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`
|
||||
sticky top-0 z-50 flex items-center justify-between px-4 py-5 sm:px-6 lg:px-8 max-w-full bg-background dark:bg-background-dark
|
||||
`}
|
||||
>
|
||||
<div className="mr-2 sm:mr-4 flex lg:hidden">
|
||||
<MobileNavigation navigation={siteConfig.navLinks} />
|
||||
</div>
|
||||
<div className="flex flex-none items-center">
|
||||
<NavbarTitle />
|
||||
<div className="hidden lg:flex ml-8 mr-6 sm:mr-8 md:mr-0">
|
||||
{siteConfig.navLinks.map((item) => (
|
||||
<NavItem item={item} key={item.name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex items-center basis-auto justify-end gap-6 xl:gap-8 md:shrink w-full">
|
||||
{/* {Search && (
|
||||
<Search>
|
||||
{({ query }) => (
|
||||
<SearchField modifierKey={modifierKey} onOpen={query?.toggle} />
|
||||
)}
|
||||
</Search>
|
||||
)} */}
|
||||
<ThemeSelector />
|
||||
{siteConfig.github && (
|
||||
<Link
|
||||
href={siteConfig.github}
|
||||
target="_blank"
|
||||
className="group"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<GitHubIcon className="h-6 w-6 dark:fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
|
||||
</Link>
|
||||
)}
|
||||
{siteConfig.discord && (
|
||||
<Link
|
||||
href={siteConfig.discord}
|
||||
className="group"
|
||||
aria-label="Discord"
|
||||
target="_blank"
|
||||
>
|
||||
<DiscordIcon className="h-8 w-8 dark:fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
80
site/components/NavItem.tsx
Normal file
80
site/components/NavItem.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
|
||||
import BaseLink from "./BaseLink";
|
||||
|
||||
export default function NavItem({ item }) {
|
||||
const dropdownRef = useRef(null);
|
||||
const [showDropdown, setshowDropdown] = useState(false);
|
||||
|
||||
const timeoutDuration = 200;
|
||||
let timeoutId;
|
||||
|
||||
const openDropdown = () => {
|
||||
clearTimeout(timeoutId);
|
||||
setshowDropdown(true);
|
||||
};
|
||||
const closeDropdown = () => {
|
||||
timeoutId = setTimeout(() => setshowDropdown(false), timeoutDuration);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button
|
||||
onClick={() => setshowDropdown(!showDropdown)}
|
||||
onMouseEnter={openDropdown}
|
||||
onMouseLeave={closeDropdown}
|
||||
>
|
||||
{Object.prototype.hasOwnProperty.call(item, "href") ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="text-slate-500 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-600"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-slate-500 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-600 fill-slate-500 hover:fill-slate-600">
|
||||
{item.name}
|
||||
</div>
|
||||
)}
|
||||
</Menu.Button>
|
||||
|
||||
{Object.prototype.hasOwnProperty.call(item, "subItems") && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={showDropdown}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="transform opacity-0 scale-5"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-5"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute top-5 flex flex-col dark:bg-slate-900/95 backdrop-blur"
|
||||
ref={dropdownRef}
|
||||
onMouseEnter={openDropdown}
|
||||
onMouseLeave={closeDropdown}
|
||||
>
|
||||
{item.subItems.map((subItem) => (
|
||||
// TODO: check the onClick error below
|
||||
// onClick does not exist on Menu.Item
|
||||
<Menu.Item
|
||||
key={subItem.name}
|
||||
onClick={() => setshowDropdown(false)}
|
||||
>
|
||||
<BaseLink
|
||||
href={subItem.href}
|
||||
className="text-slate-500 inline-flex items-center mt-2 px-1 pt-1 text-sm font-medium hover:text-slate-600"
|
||||
>
|
||||
{subItem.name}
|
||||
</BaseLink>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
37
site/components/ThemeSelector.tsx
Normal file
37
site/components/ThemeSelector.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { siteConfig } from "../config/siteConfig";
|
||||
|
||||
export default function ThemeSelector() {
|
||||
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;
|
||||
|
||||
if (!siteConfig.theme.default) 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={siteConfig.theme.toggleIcon}
|
||||
alt="toggle theme"
|
||||
width={24}
|
||||
height={24}
|
||||
className="max-w-24 max-h-24"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user