mirror of
https://github.com/visortelle/haskell-spotlight.git
synced 2024-10-05 15:47:33 +03:00
Implement package versions page
This commit is contained in:
parent
00c6f192c0
commit
ee5ad44c22
@ -12,7 +12,6 @@
|
||||
background: var(--purple-color-2);
|
||||
color: #fff;
|
||||
font-size: 14rem;
|
||||
font-weight: bold;
|
||||
border-radius: 24rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
44
hackage-ui/components/pages/package/BriefInfo.module.css
Normal file
44
hackage-ui/components/pages/package/BriefInfo.module.css
Normal file
@ -0,0 +1,44 @@
|
||||
.briefInfo {
|
||||
background-color: #fff;
|
||||
padding: 32rem 24rem 18rem 24rem;
|
||||
}
|
||||
|
||||
.packageName {
|
||||
margin: 0;
|
||||
margin-bottom: 8rem;
|
||||
line-height: 1.1;
|
||||
font-size: 32rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.packageNameH1 {
|
||||
display: inline;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.packageVersion {
|
||||
font-size: 24rem;
|
||||
margin-left: 8rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.66;
|
||||
position: relative;
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
.shortDescription {
|
||||
font-size: 18rem;
|
||||
opacity: 0.66;
|
||||
margin-bottom: 18rem;
|
||||
}
|
||||
|
||||
.shortDescription:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
22
hackage-ui/components/pages/package/BriefInfo.tsx
Normal file
22
hackage-ui/components/pages/package/BriefInfo.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import s from './BriefInfo.module.css';
|
||||
|
||||
export type BriefInfoProps = {
|
||||
packageName: string | null,
|
||||
packageVersion: string | null,
|
||||
shortDescription: string | null
|
||||
}
|
||||
|
||||
const BriefInfo = (props: BriefInfoProps) => {
|
||||
return (
|
||||
<div className={s.briefInfo}>
|
||||
<div className={s.packageName}>
|
||||
<small style={{ position: 'relative', top: '2rem' }}>📦</small>
|
||||
{props.packageName && <h1 className={s.packageNameH1}>{props.packageName}</h1>}
|
||||
{props.packageVersion && <span className={s.packageVersion}>{props.packageVersion}</span>}
|
||||
</div>
|
||||
{props.shortDescription && <div className={s.shortDescription}>{props.shortDescription}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BriefInfo;
|
@ -9,6 +9,26 @@
|
||||
width: var(--max-content-width);
|
||||
max-width: calc(100vw - 24rem);
|
||||
margin: 0 auto;
|
||||
margin-top: 24rem;
|
||||
}
|
||||
|
||||
.briefInfo {
|
||||
border-radius: 12rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0rem 2rem 4rem rgb(0 0 0 / 27%);
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
border-radius: 12rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@ -16,11 +36,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebarContainer {
|
||||
.sidebar {
|
||||
width: 240rem;
|
||||
margin: 24rem 0 24rem 18rem;
|
||||
display: flex;
|
||||
padding-top: calc(32rem + 1em);
|
||||
padding-top: 18rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
@ -33,7 +53,7 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebarContainer {
|
||||
.sidebar {
|
||||
width: 50%;
|
||||
padding-top: 0;
|
||||
margin: 24rem 18rem;
|
||||
@ -41,7 +61,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.sidebarContainer {
|
||||
.sidebar {
|
||||
width: auto;
|
||||
}
|
||||
}
|
76
hackage-ui/components/pages/package/Layout.tsx
Normal file
76
hackage-ui/components/pages/package/Layout.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import GlobalMenu, { defaultMenuProps } from "../../layout/GlobalMenu";
|
||||
import Footer from "../../layout/Footer";
|
||||
import s from './Layout.module.css';
|
||||
import { PackageProps } from "./common";
|
||||
import Sidebar from './Sidebar';
|
||||
import Tabs, { Tab } from "./tabs/Tabs";
|
||||
import BriefInfo from "./BriefInfo";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type LayoutProps = {
|
||||
analytics: {
|
||||
screenName: string
|
||||
},
|
||||
package: PackageProps,
|
||||
activeTab: string,
|
||||
children: ReactNode,
|
||||
}
|
||||
|
||||
const getTabs = (props: LayoutProps): Tab[] => {
|
||||
return [
|
||||
{
|
||||
id: 'overview',
|
||||
title: 'Overview',
|
||||
href: `/package/${props.package.id}`
|
||||
},
|
||||
// {
|
||||
// id: 'docs',
|
||||
// title: '📘 Docs',
|
||||
// href: `#`
|
||||
// },
|
||||
{
|
||||
id: 'versions',
|
||||
title: `${props.package.versionsCount} Versions`,
|
||||
href: `/package/${props.package.id}/versions`
|
||||
},
|
||||
// {
|
||||
// id: 'dependencies',
|
||||
// title: 'Dependencies',
|
||||
// href: '#'
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
const Layout = (props: LayoutProps) => {
|
||||
return (
|
||||
<div className={s.page}>
|
||||
<GlobalMenu {...defaultMenuProps} />
|
||||
<div className={s.packageContainer}>
|
||||
<div className={s.contentContainer}>
|
||||
<div className={s.content}>
|
||||
<div className={s.briefInfo}>
|
||||
<BriefInfo packageName={props.package.name} packageVersion={props.package.currentVersion} shortDescription={props.package.shortDescription} />
|
||||
<Tabs
|
||||
tabs={getTabs(props)}
|
||||
activeTab={props.activeTab}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={s.children}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={s.sidebar}>
|
||||
<Sidebar package={props.package} analytics={{ screenName: props.analytics.screenName }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.footer}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
@ -1,60 +1,25 @@
|
||||
.package {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 12rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0rem 2rem 4rem rgb(0 0 0 / 27%);
|
||||
margin-top: 24rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 24rem auto;
|
||||
box-shadow: 0rem 2rem 4rem rgb(0 0 0 / 27%);
|
||||
border-radius: 12rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.packageName {
|
||||
margin: 0;
|
||||
font-size: 32rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.packageNameH1 {
|
||||
display: inline;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.briefInfo {
|
||||
background-color: #fff;
|
||||
padding: 32rem 24rem 24rem 24rem;
|
||||
}
|
||||
|
||||
.packageVersion {
|
||||
font-size: 24rem;
|
||||
margin-left: 8rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.66;
|
||||
position: relative;
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
.shortDescription {
|
||||
font-size: 18rem;
|
||||
opacity: 0.66;
|
||||
margin-bottom: 18rem;
|
||||
}
|
||||
|
||||
.shortDescription:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.longDescription {
|
||||
max-width: var(--max-content-width);
|
||||
margin: 0 auto;
|
||||
padding: 32rem 24rem;
|
||||
background-color: #fff;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.longDescription p {
|
19
hackage-ui/components/pages/package/OverviewPage.tsx
Normal file
19
hackage-ui/components/pages/package/OverviewPage.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import s from './OverviewPage.module.css';
|
||||
import { PackageProps } from "./common";
|
||||
import Layout from "./Layout";
|
||||
|
||||
const screenName = 'PackageOverviewPage';
|
||||
|
||||
const OverviewPage = (props: PackageProps) => {
|
||||
return (
|
||||
<Layout analytics={{ screenName }} package={props} activeTab="overview">
|
||||
<div className={s.package}>
|
||||
<div className={s.content}>
|
||||
{props.longDescriptionHtml && <div className={s.longDescription} dangerouslySetInnerHTML={{ __html: props.longDescriptionHtml }}></div>}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverviewPage;
|
@ -1,33 +0,0 @@
|
||||
import GlobalMenu, { defaultMenuProps } from "../../layout/GlobalMenu";
|
||||
import Footer from "../../layout/Footer";
|
||||
import s from './PackagePage.module.css';
|
||||
import { PackageProps } from "./common";
|
||||
import Sidebar from './Sidebar';
|
||||
import PackageOverview from "./tabs/PackageOverview";
|
||||
|
||||
const screenName = 'HackagePackagePage';
|
||||
|
||||
const Package = (props: PackageProps) => {
|
||||
return (
|
||||
<div className={s.page}>
|
||||
<GlobalMenu {...defaultMenuProps} />
|
||||
<div className={s.packageContainer}>
|
||||
<Tabs {...props} />
|
||||
<div className={s.sidebarContainer}>
|
||||
<Sidebar package={props} analytics={{ screenName }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.footer}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Tabs = (props: PackageProps) => {
|
||||
return (
|
||||
<PackageOverview {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default Package;
|
@ -23,7 +23,7 @@ type SidebarProps = {
|
||||
export const Sidebar = (props: SidebarProps) => {
|
||||
const appContext = useContext(lib.appContext.AppContext);
|
||||
const repository = props.package.repositoryUrl ? parseRepositoryUrl(props.package.repositoryUrl) : null;
|
||||
const copyToInstall = `${props.package.name} >= ${props.package.versions.current}`;
|
||||
const copyToInstall = `${props.package.name} >= ${props.package.currentVersion}`;
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
@ -76,7 +76,7 @@ export const Sidebar = (props: SidebarProps) => {
|
||||
</div>
|
||||
|
||||
{
|
||||
props.package.homepage &&
|
||||
props.package.homepageUrl &&
|
||||
<div className={s.sidebarSection}>
|
||||
<h3 className={s.sidebarSectionHeader}>
|
||||
Homepage
|
||||
@ -85,10 +85,10 @@ export const Sidebar = (props: SidebarProps) => {
|
||||
<div className={s.sidebarEntryIcon}><SvgIcon svg={homepageIcon} /></div>
|
||||
<lib.links.ExtA
|
||||
className={s.sidebarEntryLink}
|
||||
href={props.package.homepage.url}
|
||||
href={props.package.homepageUrl.url}
|
||||
analytics={{ featureName: 'GoToPackageHomepage', eventParams: { screen_name: props.analytics.screenName } }}
|
||||
>
|
||||
{props.package.homepage.text.replace(/^https?\:\/\//, '').replace(/\/$/, '')}
|
||||
{props.package.homepageUrl.text.replace(/^https?\:\/\//, '').replace(/\/$/, '')}
|
||||
</lib.links.ExtA>
|
||||
</div>
|
||||
</div>
|
||||
|
45
hackage-ui/components/pages/package/VersionsPage.module.css
Normal file
45
hackage-ui/components/pages/package/VersionsPage.module.css
Normal file
@ -0,0 +1,45 @@
|
||||
.versionsPage {
|
||||
padding-bottom: 24rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 24rem 0 8rem 0;
|
||||
font-size: 14rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.version {
|
||||
padding: 18rem 24rem;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12rem;
|
||||
box-shadow: 0rem 2rem 4rem rgb(0 0 0 / 27%);
|
||||
border-radius: 12rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.version:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.versionId {
|
||||
font-weight: bold;
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.versionKind {
|
||||
width: 120rem;
|
||||
height: 24rem;
|
||||
border-radius: 8rem;
|
||||
margin-right: 24rem;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--surface-color);
|
||||
font-size: 12rem;
|
||||
}
|
118
hackage-ui/components/pages/package/VersionsPage.tsx
Normal file
118
hackage-ui/components/pages/package/VersionsPage.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import s from './VersionsPage.module.css';
|
||||
import { PackageProps, Versions } from './common';
|
||||
import Layout from './Layout';
|
||||
import * as lib from '@hackage-ui/react-lib';
|
||||
|
||||
const screenName = 'PackageVersionsPage';
|
||||
|
||||
export type VersionsPageProps = {
|
||||
package: PackageProps,
|
||||
}
|
||||
|
||||
const VersionsPage = (props: VersionsPageProps) => {
|
||||
const versions = props.package.versions ? sortVersions(Array.from(new Set([
|
||||
...props.package.versions?.normal,
|
||||
...props.package.versions?.unpreferred,
|
||||
...props.package.versions?.deprecated,
|
||||
]))) : [];
|
||||
|
||||
return (
|
||||
<Layout analytics={{ screenName }} package={props.package} activeTab="versions">
|
||||
<div className={s.versionsPage}>
|
||||
<div className={s.info}>
|
||||
<span>All {versions.length} versions of <strong>{props.package.name}</strong></span>
|
||||
|
||||
<lib.links.ExtA
|
||||
href="https://pvp.haskell.org/"
|
||||
analytics={{ featureName: 'GoToPackageVersioningPolicy', eventParams: { screen_name: screenName } }}
|
||||
>
|
||||
📘 Versioning Policy
|
||||
</lib.links.ExtA>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{versions.map(versionId => {
|
||||
let kind: VersionKind = 'normal';
|
||||
if (props.package.versions && props.package.versions?.unpreferred.includes(versionId)) {
|
||||
kind = 'unpreferred'
|
||||
} else if (props.package.versions && props.package.versions?.deprecated.includes(versionId)) {
|
||||
kind = 'deprecated'
|
||||
}
|
||||
|
||||
return (
|
||||
<Version
|
||||
key={versionId}
|
||||
id={versionId}
|
||||
kind={kind}
|
||||
getHref={() => `/package/${props.package.name}-${versionId}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
type VersionKind = 'normal' | 'unpreferred' | 'deprecated';
|
||||
export type VersionProps = {
|
||||
id: string,
|
||||
kind: VersionKind,
|
||||
getHref: () => string
|
||||
}
|
||||
|
||||
const Version = (props: VersionProps) => {
|
||||
let versionKindColor = 'var(--accent-color-green)';
|
||||
let versionKindLabel = 'Recommended';
|
||||
if (props.kind === 'deprecated') {
|
||||
versionKindColor = 'var(--accent-color-red)';
|
||||
versionKindLabel = 'Deprecated';
|
||||
} else if (props.kind === 'unpreferred') {
|
||||
versionKindColor = 'var(--text-color)';
|
||||
versionKindLabel = 'Unpreferred';
|
||||
}
|
||||
|
||||
return (
|
||||
<lib.links.A
|
||||
className={s.version}
|
||||
href={props.getHref()}
|
||||
analytics={{ featureName: 'ClickPackageVersion', eventParams: { event_label: props.id, screen_name: screenName } }}
|
||||
>
|
||||
<div
|
||||
className={s.versionKind}
|
||||
style={{
|
||||
background: versionKindColor
|
||||
}}
|
||||
>
|
||||
{versionKindLabel}
|
||||
</div>
|
||||
<div
|
||||
className={s.versionId}
|
||||
style={{ color: versionKindColor }}
|
||||
>
|
||||
{props.id}
|
||||
</div>
|
||||
</lib.links.A>
|
||||
);
|
||||
}
|
||||
|
||||
function sortVersions(versions: string[]): string[] {
|
||||
return versions.sort((v1, v2) => {
|
||||
const v1parts = v1.split('.');
|
||||
const v2parts = v2.split('.');
|
||||
const len = Math.min(v1parts.length, v2parts.length);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a2 = +v1parts[i] || 0;
|
||||
const b2 = +v2parts[i] || 0;
|
||||
|
||||
if (a2 !== b2) {
|
||||
return a2 > b2 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
return v1.length - v2.length;
|
||||
}).reverse();
|
||||
}
|
||||
|
||||
export default VersionsPage;
|
@ -1,28 +1,31 @@
|
||||
export type Versions = {
|
||||
current: string,
|
||||
available: string[],
|
||||
}
|
||||
normal: string[];
|
||||
unpreferred: string[];
|
||||
deprecated: string[];
|
||||
};
|
||||
|
||||
export type License = {
|
||||
name: string,
|
||||
url: string | null
|
||||
}
|
||||
name: string;
|
||||
url: string | null;
|
||||
};
|
||||
|
||||
export type Homepage = {
|
||||
text: string,
|
||||
url: string
|
||||
}
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type PackageProps = {
|
||||
id: string,
|
||||
name: string,
|
||||
license: License | null,
|
||||
homepage: Homepage | null,
|
||||
repositoryUrl: string | null,
|
||||
bugReportsUrl: string | null,
|
||||
versions: Versions,
|
||||
shortDescription: string | null,
|
||||
longDescriptionHtml: string | null,
|
||||
id: string;
|
||||
name: string;
|
||||
license: License | null;
|
||||
homepageUrl: Homepage | null;
|
||||
repositoryUrl: string | null;
|
||||
bugReportsUrl: string | null;
|
||||
versions: Versions | null;
|
||||
currentVersion: string | null;
|
||||
versionsCount: number,
|
||||
shortDescription: string | null;
|
||||
longDescriptionHtml: string | null;
|
||||
// Date in ISO 8601
|
||||
updatedAt: string | null
|
||||
}
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
@ -1,20 +0,0 @@
|
||||
import s from './PackageOverview.module.css';
|
||||
import { PackageProps } from "../common";
|
||||
|
||||
const PackageOverview = (props: PackageProps) => {
|
||||
return (
|
||||
<div className={s.package}>
|
||||
<div className={s.content}>
|
||||
<div className={s.briefInfo}>
|
||||
<div className={s.packageName}>
|
||||
<small style={{ position: 'relative', top: '2rem' }}>📦</small> <h1 className={s.packageNameH1}>{props.name}</h1><span className={s.packageVersion}>{props.versions.current}</span>
|
||||
</div>
|
||||
{props.shortDescription && <div className={s.shortDescription}>{props.shortDescription}</div>}
|
||||
{props.longDescriptionHtml && <div className={s.longDescription} dangerouslySetInnerHTML={{ __html: props.longDescriptionHtml }}></div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PackageOverview;
|
38
hackage-ui/components/pages/package/tabs/Tabs.module.css
Normal file
38
hackage-ui/components/pages/package/tabs/Tabs.module.css
Normal file
@ -0,0 +1,38 @@
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--purple-color-2);
|
||||
border-top: 2rem solid var(--purple-color-1);
|
||||
}
|
||||
|
||||
.tabPicker {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabPickerItemContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabPickerItem {
|
||||
display: inline-flex;
|
||||
padding: 12rem 24rem;
|
||||
font-size: 14rem;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #fff;
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.tabPickerItem:hover {
|
||||
cursor: pointer;
|
||||
transition: var(--transition-short);
|
||||
}
|
||||
|
||||
.tabPickerItem:hover,
|
||||
.tabPickerItemActive {
|
||||
opacity: 1;
|
||||
}
|
39
hackage-ui/components/pages/package/tabs/Tabs.tsx
Normal file
39
hackage-ui/components/pages/package/tabs/Tabs.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { ReactNode } from "react";
|
||||
import s from './Tabs.module.css';
|
||||
import * as lib from '@hackage-ui/react-lib';
|
||||
|
||||
export type Tab = {
|
||||
id: string,
|
||||
title: string,
|
||||
href: string,
|
||||
};
|
||||
|
||||
export type TabsProps = {
|
||||
tabs: Tab[],
|
||||
activeTab: string,
|
||||
}
|
||||
|
||||
const Tabs = (props: TabsProps) => {
|
||||
return (
|
||||
<div className={s.tabs}>
|
||||
<div className={s.tabPicker}>
|
||||
{props.tabs.map(tab => {
|
||||
const isActive = tab.id === props.activeTab;
|
||||
return (
|
||||
<div key={tab.id} className={s.tabPickerItemContainer}>
|
||||
<lib.links.A
|
||||
className={`${s.tabPickerItem} ${isActive ? s.tabPickerItemActive : ''}`}
|
||||
href={tab.href}
|
||||
analytics={{ featureName: `ClickTab-${tab.id}`, eventParams: {} }}
|
||||
>
|
||||
{tab.title}
|
||||
</lib.links.A>
|
||||
</div >
|
||||
);
|
||||
})}
|
||||
</div >
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
export default Tabs;
|
206
hackage-ui/fetch/package.tsx
Normal file
206
hackage-ui/fetch/package.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
import axios from 'axios';
|
||||
import hljs from 'highlight.js';
|
||||
import { License, Homepage, Versions } from '../components/pages/package/common';
|
||||
import cheerio, { CheerioAPI } from 'cheerio';
|
||||
|
||||
export type Package = {
|
||||
id: string,
|
||||
name: string,
|
||||
versions: Versions | null,
|
||||
currentVersion: string | null,
|
||||
versionsCount: number,
|
||||
shortDescription: string | null,
|
||||
longDescriptionHtml: string | null,
|
||||
license: License | null,
|
||||
homepageUrl: Homepage | null,
|
||||
repositoryUrl: string | null,
|
||||
bugReportsUrl: string | null,
|
||||
updatedAt: string | null,
|
||||
}
|
||||
|
||||
export async function getPackage(packageId: string): Promise<Package> {
|
||||
let html = '';
|
||||
try {
|
||||
html = await getPackageRawHtml(packageId);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
monkeyPatchDocument($);
|
||||
|
||||
const docContent = $('#content');
|
||||
|
||||
const name = $('h1 a', docContent).html()?.trim() || '';
|
||||
const shortDescription = $('h1 small', docContent).html()?.trim() || null;
|
||||
const longDescriptionHtml = $('#description').html()?.trim() || null;
|
||||
|
||||
const versions = await getVersions(name);
|
||||
const versionsCount = Array.from(new Set([
|
||||
...versions.normal,
|
||||
...versions.unpreferred,
|
||||
...versions.deprecated,
|
||||
])).length;
|
||||
|
||||
return {
|
||||
id: packageId,
|
||||
name,
|
||||
currentVersion: getCurrentVersion($),
|
||||
versions,
|
||||
versionsCount,
|
||||
bugReportsUrl: getBugReportsUrl($),
|
||||
homepageUrl: getHomepageUrl($),
|
||||
license: getLicense($),
|
||||
shortDescription,
|
||||
longDescriptionHtml,
|
||||
repositoryUrl: getRepositoryUrl($),
|
||||
updatedAt: getUpdatedAt($)
|
||||
}
|
||||
}
|
||||
|
||||
// highlight.js often recognize Haskell code as Erlang, OCaml and others.
|
||||
// We decided to specify subset of popular languages here excluding from ML family.
|
||||
// If you want to modify the list or know a better solution, please raise an issue or pull request at our issue tracker.
|
||||
// Full list here https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md
|
||||
const languagesToHighlight = [
|
||||
'Haskell',
|
||||
'Shell',
|
||||
'Bash',
|
||||
'Diff',
|
||||
'JSON',
|
||||
'LaTeX',
|
||||
'Protocol Buffers',
|
||||
'TOML',
|
||||
'XML',
|
||||
'YAML',
|
||||
'Nix'
|
||||
];
|
||||
|
||||
export async function getPackageRawHtml(packageId: string): Promise<string> {
|
||||
let html: string = '';
|
||||
|
||||
try {
|
||||
html = await (await axios(`https://hackage.haskell.org/package/${encodeURIComponent(packageId)}`)).data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// XXX - We can get rid of most content of this function after the hackage-server will implement missing APIs.
|
||||
export function monkeyPatchDocument($: CheerioAPI): void {
|
||||
// Rewrite urls.
|
||||
$('a').map((_, a) => {
|
||||
$(a).attr('href', $(a).attr('href')?.replace('https://hackage.haskell.org/package/', '/package/') || '#')
|
||||
});
|
||||
|
||||
// Highlight code blocks
|
||||
$('code, pre').map((_, el) => {
|
||||
/* Ignore blocks containing other HTML elements:
|
||||
<code><a href="http://hackage.haskell.org/package/array">array</a></code>
|
||||
*/
|
||||
if ($(el).children().length) {
|
||||
return el;
|
||||
}
|
||||
|
||||
const highlightedHtml = hljs.highlightAuto(unescape($(el).html() as string), languagesToHighlight).value;
|
||||
$(el).html(highlightedHtml);
|
||||
$(el).addClass('hljs');
|
||||
});
|
||||
|
||||
// Remove "Skip to Readme" links.
|
||||
const description = $('#content #description');
|
||||
const newDescriptionHtml = (description.html() || '').replace(`<hr>
|
||||
[<a href="#readme">Skip to Readme</a>]`, '').trim();
|
||||
description.html(newDescriptionHtml);
|
||||
}
|
||||
|
||||
export function getCurrentVersion($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Version') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $(`strong`, tableTd).text().trim();
|
||||
}
|
||||
|
||||
export function getLicense($: CheerioAPI): License | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('License') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const licenseEl = $(`> *`, tableTd);
|
||||
return { name: licenseEl.text(), url: licenseEl.attr('href') || null };
|
||||
}
|
||||
|
||||
export function getHomepageUrl($: CheerioAPI): Homepage | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Home page') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const homepageLink = $(`a`, tableTd);
|
||||
return {
|
||||
text: homepageLink.text().trim(),
|
||||
url: homepageLink.attr('href')?.trim() || '#'
|
||||
};
|
||||
}
|
||||
|
||||
export function getRepositoryUrl($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Source') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repositoryLink = $(`a`, tableTd);
|
||||
return repositoryLink.attr('href')?.trim() || null;
|
||||
}
|
||||
|
||||
export function getBugReportsUrl($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Bug') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repositoryLink = $(`a`, tableTd);
|
||||
return repositoryLink.attr('href')?.trim() || null;
|
||||
}
|
||||
|
||||
export function getUpdatedAt($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Uploaded') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $(tableTd).text().replace(/^by .* at /, '').trim();
|
||||
}
|
||||
|
||||
export async function getVersions(packageName: string): Promise<Versions> {
|
||||
let versions: Versions = { normal: [], unpreferred: [], deprecated: [] };
|
||||
try {
|
||||
const data = await (await axios.get(`https://hackage.haskell.org/package/${packageName}/preferred`, { headers: { accept: 'application/json' } })).data;
|
||||
versions = {
|
||||
normal: data['normal-version'] || [],
|
||||
unpreferred: data['unpreferred-version'] || [],
|
||||
deprecated: data['deprecated-version'] || [],
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
return versions;
|
||||
}
|
||||
}
|
18
hackage-ui/package-lock.json
generated
18
hackage-ui/package-lock.json
generated
@ -15177,6 +15177,12 @@
|
||||
"is-core-module": "^2.2.0",
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -16003,6 +16009,13 @@
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
@ -16784,11 +16797,6 @@
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { NextPage, GetStaticPropsResult, GetStaticPropsContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
import PackagePage from '../../components/pages/package/PackagePage';
|
||||
import { PackageProps, Versions, License, Homepage } from '../../components/pages/package/common';
|
||||
import axios from 'axios';
|
||||
import hljs from 'highlight.js';
|
||||
import cheerio, { CheerioAPI } from 'cheerio';
|
||||
import unescape from 'lodash/unescape';
|
||||
import OverviewPage from '../../components/pages/package/OverviewPage';
|
||||
import { PackageProps } from '../../components/pages/package/common';
|
||||
import * as pkgFetch from '../../fetch/package';
|
||||
|
||||
const Page: NextPage<PackageProps> = (props) => {
|
||||
return (
|
||||
@ -15,43 +12,18 @@ const Page: NextPage<PackageProps> = (props) => {
|
||||
<meta name="description" content={props.shortDescription || props.name}></meta>
|
||||
</Head>
|
||||
|
||||
<PackagePage {...props} />
|
||||
<OverviewPage {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticProps(props: GetStaticPropsContext): Promise<GetStaticPropsResult<PackageProps>> {
|
||||
const packageId = props.params!.packageId as string;
|
||||
|
||||
let html = '';
|
||||
try {
|
||||
html = await (await axios(`https://hackage.haskell.org/package/${encodeURIComponent(packageId)}`)).data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
monkeyPatchDocument($);
|
||||
const docContent = $('#content');
|
||||
|
||||
const name = $('h1 a', docContent).html()?.trim() || '';
|
||||
const shortDescription = $('h1 small', docContent).html()?.trim() || null;
|
||||
const longDescriptionHtml = $('#description').html()?.trim() || null;
|
||||
const pkg = await pkgFetch.getPackage(packageId);
|
||||
|
||||
return {
|
||||
props: {
|
||||
id: packageId,
|
||||
name,
|
||||
versions: getVersions($),
|
||||
license: getLicense($),
|
||||
homepage: getHomepage($),
|
||||
repositoryUrl: getRepositoryUrl($),
|
||||
bugReportsUrl: getBugReports($),
|
||||
updatedAt: getUpdatedAt($),
|
||||
shortDescription,
|
||||
longDescriptionHtml
|
||||
},
|
||||
revalidate: 10
|
||||
props: pkg,
|
||||
revalidate: 60
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,120 +34,4 @@ export async function getStaticPaths() {
|
||||
};
|
||||
}
|
||||
|
||||
// highlight.js often recognize Haskell code as Erlang, OCaml and others.
|
||||
// We decided to specify subset of popular languages here excluding from ML family.
|
||||
// If you want to modify the list or know a better solution, please raise an issue or pull request at our issue tracker.
|
||||
// Full list here https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md
|
||||
const languagesToHighlight = [
|
||||
'Haskell',
|
||||
'Shell',
|
||||
'Bash',
|
||||
'Diff',
|
||||
'JSON',
|
||||
'LaTeX',
|
||||
'Protocol Buffers',
|
||||
'TOML',
|
||||
'XML',
|
||||
'YAML',
|
||||
'Nix'
|
||||
];
|
||||
|
||||
// XXX - We can get rid of most content of this function after the hackage-server will implement missing APIs.
|
||||
function monkeyPatchDocument($: CheerioAPI): void {
|
||||
// Rewrite urls.
|
||||
$('a').map((_, a) => {
|
||||
$(a).attr('href', $(a).attr('href')?.replace('https://hackage.haskell.org/package/', '/package/') || '#')
|
||||
});
|
||||
|
||||
// Highlight code blocks
|
||||
$('code, pre').map((_, el) => {
|
||||
/* Ignore blocks containing other HTML elements:
|
||||
<code><a href="http://hackage.haskell.org/package/array">array</a></code>
|
||||
*/
|
||||
if ($(el).children().length) {
|
||||
return el;
|
||||
}
|
||||
|
||||
const highlightedHtml = hljs.highlightAuto(unescape($(el).html() as string), languagesToHighlight).value;
|
||||
$(el).html(highlightedHtml);
|
||||
$(el).addClass('hljs');
|
||||
});
|
||||
|
||||
// Remove "Skip to Readme" links.
|
||||
const description = $('#content #description');
|
||||
const newDescriptionHtml = (description.html() || '').replace(`<hr>
|
||||
[<a href="#readme">Skip to Readme</a>]`, '').trim();
|
||||
description.html(newDescriptionHtml);
|
||||
}
|
||||
|
||||
function getVersions($: CheerioAPI): Versions {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Version') + td`, propertiesElement).get(0);
|
||||
const current = $(`strong`, tableTd).text().trim();
|
||||
const available = $('*', tableTd).map((_, el) => $(el).text().trim()).toArray();
|
||||
return { current, available };
|
||||
}
|
||||
|
||||
function getLicense($: CheerioAPI): License | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('License') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const licenseEl = $(`> *`, tableTd);
|
||||
return { name: licenseEl.text(), url: licenseEl.attr('href') || null };
|
||||
}
|
||||
|
||||
function getHomepage($: CheerioAPI): Homepage | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Home page') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const homepageLink = $(`a`, tableTd);
|
||||
return {
|
||||
text: homepageLink.text().trim(),
|
||||
url: homepageLink.attr('href')?.trim() || '#'
|
||||
};
|
||||
}
|
||||
|
||||
function getRepositoryUrl($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Source') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repositoryLink = $(`a`, tableTd);
|
||||
return repositoryLink.attr('href')?.trim() || null;
|
||||
}
|
||||
|
||||
function getBugReports($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Bug') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repositoryLink = $(`a`, tableTd);
|
||||
return repositoryLink.attr('href')?.trim() || null;
|
||||
}
|
||||
|
||||
function getUpdatedAt($: CheerioAPI): string | null {
|
||||
const propertiesElement = $('#properties').get(0);
|
||||
const tableTd = $(`th:contains('Uploaded') + td`, propertiesElement).get(0);
|
||||
|
||||
if (!tableTd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $(tableTd).text().replace(/^by .* at /, '').trim();
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
38
hackage-ui/pages/package/[packageId]/versions.tsx
Normal file
38
hackage-ui/pages/package/[packageId]/versions.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { NextPage, GetStaticPropsResult, GetStaticPropsContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
import VersionsPage, { VersionsPageProps } from '../../../components/pages/package/VersionsPage';
|
||||
import * as pkgFetch from '../../../fetch/package';
|
||||
|
||||
const Page: NextPage<VersionsPageProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{props.package.name} - Hackage: The Haskell community’s package registry</title>
|
||||
<meta name="description" content={props.package.shortDescription || props.package.name}></meta>
|
||||
</Head>
|
||||
|
||||
<VersionsPage {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticProps(props: GetStaticPropsContext): Promise<GetStaticPropsResult<VersionsPageProps>> {
|
||||
const packageId = props.params!.packageId as string;
|
||||
const pkg = await pkgFetch.getPackage(packageId);
|
||||
|
||||
return {
|
||||
props: {
|
||||
package: pkg,
|
||||
},
|
||||
revalidate: 60
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: 'blocking'
|
||||
};
|
||||
}
|
||||
|
||||
export default Page;
|
@ -19,9 +19,9 @@
|
||||
--purple-color-3: #8f4e8b;
|
||||
--transition-short: all 180ms ease 0s;
|
||||
--backdrop-filter-blur: blur(12px);
|
||||
--accent-color-green: var(--purple-color-2);
|
||||
--accent-color-green: #04c262;
|
||||
--accent-color-red: #bf4646;
|
||||
--accent-color-yellow: #ff9042;
|
||||
--accent-color-yellow: #f8cf06;
|
||||
--accent-color-blue: #5084ff;
|
||||
--accent-color-purple: #9d50ff;
|
||||
--max-content-width: 1000px;
|
||||
|
@ -19,9 +19,9 @@
|
||||
--purple-color-3: #8f4e8b;
|
||||
--transition-short: all 180ms ease 0s;
|
||||
--backdrop-filter-blur: blur(12px);
|
||||
--accent-color-green: var(--purple-color-2);
|
||||
--accent-color-green: #04c262;
|
||||
--accent-color-red: #bf4646;
|
||||
--accent-color-yellow: #ff9042;
|
||||
--accent-color-yellow: #f8cf06;
|
||||
--accent-color-blue: #5084ff;
|
||||
--accent-color-purple: #9d50ff;
|
||||
--max-content-width: 1000px;
|
||||
|
@ -18,3 +18,4 @@ https://code.visualstudio.com/api/extension-guides/webview#inspecting-and-debugg
|
||||
|
||||
https://code.visualstudio.com/api/working-with-extensions/publishing-extension
|
||||
|
||||
Manage in the marketplace: https://marketplace.visualstudio.com/manage/publishers/visortelle
|
||||
|
@ -1,3 +1,10 @@
|
||||
# Haskell Spotlight
|
||||
|
||||
Search on Hackage, Hoogle, and more soon.
|
||||
|
||||
`Alt + H` is a default hotkey.
|
||||
|
||||
<img width="1437" alt="Screen Shot 2022-01-18 at 10 18 26 PM" src="https://user-images.githubusercontent.com/9302460/150020345-0a0146a3-1f89-4d78-a099-b73c31c50f62.png">
|
||||
|
||||
https://user-images.githubusercontent.com/9302460/150015894-fe62ea7d-9a45-4e31-842a-e60ae8747970.mov
|
||||
|
||||
|
2
vscode-extension/package-lock.json
generated
2
vscode-extension/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "haskell-spotlight",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "haskell-spotlight",
|
||||
"description": "Search on Hackage, Hoogle, and more soon.",
|
||||
"icon": "build/icon-192.png",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"publisher": "visortelle",
|
||||
"displayName": "Haskell Spotlight",
|
||||
"license": "MIT",
|
||||
|
Loading…
Reference in New Issue
Block a user