import React = require("react");
import { useState, useEffect } from "react";
import { useAuth } from "../../useAuth";
import Nav from "../../sections/Nav";
import Loading from "../Loading";
import ErrorPage from "../ErrorPage";
import {
BoxSeam,
HouseDoor,
Book,
Building,
Calendar3,
Check2Square,
Hdd,
CheckSquare,
Check,
Square,
} from "react-bootstrap-icons";
import { useParams, NavLink, Redirect } from "react-router-dom";
import { useAuthenticatedRequest } from "../../util";
import Prism from "react-syntax-highlighter/dist/cjs/prism";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import Members from "./Members";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import HumanTime from "react-human-time";
type Tab = "readme" | "versions" | "members";
export interface CrateInfo {
name: string;
readme?: string;
description?: string;
repository?: string;
homepage?: string;
documentation?: string;
versions: CrateInfoVersion[];
}
export interface CrateInfoVersion {
vers: string;
deps: CrateInfoVersionDependency[];
features: any[];
size: number;
uploader: string;
created_at: string;
}
export interface CrateInfoVersionDependency {
name: string;
version_req: string;
}
export default function SingleCrate() {
const auth = useAuth();
const { organisation, crate, subview } = useParams();
const currentTab: Tab | undefined = subview;
if (!currentTab) {
return <Redirect to={`/crates/${organisation}/${crate}/readme`} />;
}
const { response: crateInfo, error } = useAuthenticatedRequest<CrateInfo>({
auth,
endpoint: `crates/${organisation}/${crate}`,
});
if (error) {
return <ErrorPage message={error} />;
} else if (!crateInfo) {
return <Loading />;
}
const crateVersion = crateInfo.versions[crateInfo.versions.length - 1];
return (
<div className="text-white">
<Nav />
<div className="container mt-4 pb-4">
<div className="row align-items-stretch">
<div className="col-12 col-md-6 mb-3 mb-md-0">
<div className="card border-0 shadow-sm text-black h-100">
<div className="card-body">
<div className="d-flex flex-row align-items-center">
<div
className="text-white circle bg-primary bg-gradient d-inline rounded-circle d-inline-flex justify-content-center align-items-center"
style={{ width: "2rem", height: "2rem" }}
>
<BoxSeam />
</div>
<h1 className="text-primary d-inline px-2">{crate}</h1>
<h2 className="text-secondary m-0">{crateVersion.vers}</h2>
</div>
<p className="m-0">{crateInfo.description}</p>
</div>
</div>
</div>
<div className="col-12 col-md-6">
<div className="card border-0 shadow-sm text-black h-100">
<div className="card-body d-flex flex-column justify-content-center">
{crateInfo.homepage ? (
<div>
<HouseDoor />{" "}
<a href={crateInfo.homepage}>{crateInfo.homepage}</a>
</div>
) : (
<></>
)}
{crateInfo.documentation ? (
<div>
<Book />{" "}
<a href={crateInfo.documentation}>
{crateInfo.documentation}
</a>
</div>
) : (
<></>
)}
{crateInfo.repository ? (
<div>
<Building />{" "}
<a href={crateInfo.repository}>{crateInfo.repository}</a>
</div>
) : (
<></>
)}
</div>
</div>
</div>
</div>
<div className="row my-4">
<div className="col-12 col-md-9 mb-3 mb-md-0">
<div className="card border-0 shadow-sm text-black">
<div className="card-header">
<ul className="nav nav-pills card-header-pills">
<li className="nav-item">
<NavLink
to={`/crates/${organisation}/${crate}/readme`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Readme
</NavLink>
</li>
<li className="nav-item">
<NavLink
to={`/crates/${organisation}/${crate}/versions`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Versions
<span className={`badge rounded-pill bg-danger ms-1`}>
{crateInfo.versions.length}
</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink
to={`/crates/${organisation}/${crate}/members`}
className="nav-link"
activeClassName="bg-primary bg-gradient active"
>
Members
</NavLink>
</li>
</ul>
</div>
<div className="card-body">
{currentTab == "readme" ? <ReadMe crate={crateInfo} /> : <></>}
{currentTab == "versions" ? (
<Versions crate={crateInfo} />
) : (
<></>
)}
{currentTab == "members" ? (
<Members crate={crate} organisation={organisation} />
) : (
<></>
)}
</div>
</div>
</div>
<div className="col-12 col-md-3">
<div className="card border-0 shadow-sm text-black">
<div className="card-body pb-0">
<h5 className="card-title">Dependencies</h5>
</div>
<ul className="list-group list-group-flush mb-2">
{crateVersion.deps.map((dep) => (
<li
key={`${dep.name}-${dep.version_req}`}
className="list-group-item"
>
{dep.name} = "<strong>{dep.version_req}</strong>"
</li>
))}
</ul>
</div>
<div className="card border-0 shadow-sm text-black mt-4">
<div className="card-body pb-0">
<h5 className="card-title">Dependents</h5>
</div>
<ul className="list-group list-group-flush">
<li className="list-group-item">An item</li>
<li className="list-group-item">A second item</li>
<li className="list-group-item">A third item</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}
function Versions(props: { crate: CrateInfo }) {
const humanFileSize = (size) => {
var i = Math.floor(Math.log(size) / Math.log(1024));
return (
Number((size / Math.pow(1024, i)).toFixed(2)) +
" " +
["B", "kB", "MB", "GB", "TB"][i]
);
};
return (
<div>
{[...props.crate.versions].reverse().map((version, index) => (
<div
key={index}
className={`card text-white bg-gradient ${
index == 0 ? "bg-primary" : "bg-dark mt-2"
}`}
>
<div className="card-body d-flex align-items-center">
<h5 className="m-0">{version.vers}</h5>
<div className="text-uppercase ms-4" style={{ fontSize: ".75rem" }}>
<div>
<div className="d-inline-block">
By
<img
src="http://placekitten.com/22/22"
className="rounded-circle ms-1 me-1"
/>
{version.uploader}
</div>
<div className="ms-3 d-inline-block">
<OverlayTrigger
overlay={
<Tooltip
id={`tooltip-${props.crate.name}-version-${version.vers}-date`}
>
{new Date(version.created_at).toLocaleString()}
</Tooltip>
}
>
<span>
<Calendar3 />{" "}
<HumanTime
time={new Date(version.created_at).getTime()}
/>
</span>
</OverlayTrigger>
</div>
</div>
<div>
<div className="d-inline-block">
<Hdd /> {humanFileSize(version.size)}
</div>
<div className="ms-3 d-inline-block">
<OverlayTrigger
overlay={
<Tooltip
id={`tooltip-${props.crate.name}-version-${version.vers}-feature-${index}`}
>
<div className="text-start m-2">
{Object.keys(version.features).map(
(feature, index) => (
<div key={index}>
{version.features["default"].includes(
feature
) ? (
<CheckSquare className="me-2" />
) : (
<Square className="me-2" />
)}
{feature}
</div>
)
)}
</div>
</Tooltip>
}
>
<span>
<Check2Square /> {Object.keys(version.features).length}{" "}
Features
</span>
</OverlayTrigger>
</div>
</div>
</div>
</div>
</div>
))}
</div>
);
}
function ReadMe(props: { crate: CrateInfo }) {
return (
<ReactMarkdown
children={props.crate.readme}
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<Prism
children={String(children).replace(/\n$/, "")}
language={match[1]}
PreTag="pre"
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
/>
);
}