diff --git a/src/app/ExternalRedirect.js b/src/app/ExternalRedirect.js
index 1f59aec..a53d8fd 100644
--- a/src/app/ExternalRedirect.js
+++ b/src/app/ExternalRedirect.js
@@ -2,6 +2,8 @@
import React from "react";
+import Link from "./Link";
+
export default class ExternalRedirect extends React.Component<{|
+redirectTo: string,
|}> {
@@ -11,7 +13,7 @@ export default class ExternalRedirect extends React.Component<{|
Redirecting…
Redirecting to:{" "}
- {this.props.redirectTo}
+ {this.props.redirectTo}
);
diff --git a/src/app/HomePage.js b/src/app/HomePage.js
index 0282aaf..d821d7b 100644
--- a/src/app/HomePage.js
+++ b/src/app/HomePage.js
@@ -1,10 +1,9 @@
// @flow
-import {StyleSheet, css} from "aphrodite/no-important";
-import React, {type Node} from "react";
-import {Link} from "react-router";
+import React from "react";
import type {Assets} from "./assets";
+import Link from "./Link";
export default class HomePage extends React.Component<{|+assets: Assets|}> {
render() {
@@ -44,12 +43,13 @@ export default class HomePage extends React.Component<{|+assets: Assets|}> {
Despite all the value provided by open-source projects, many are
chronically underfunded. For example, NumPy{" "}
- received no funding at all until 2017 ,
- and{" "}
-
+
+ received no funding at all until 2017
+ , and{" "}
+
a world where OpenSSL was funded might have been a world without
Heartbleed
- .
+ .
@@ -115,12 +115,14 @@ export default class HomePage extends React.Component<{|+assets: Assets|}> {
How cred works
Cred is computed by first creating a contribution{" "}
- graph
+ graph
, which contains every contribution to the project and the relations
among them. For example, GitHub issues, Git commits, and individual
files and functions can be included in the graph. Then, SourceCred
- runs a modified version of PageRank on
- that graph to produce a cred attribution. The attribution is highly
+ runs a modified version of
+ PageRank
+ {" "}
+ on that graph to produce a cred attribution. The attribution is highly
configurable; project maintainers can add new heuristics and adjust
weights.
@@ -145,13 +147,11 @@ export default class HomePage extends React.Component<{|+assets: Assets|}> {
Roadmap
SourceCred is under active development.{" "}
-
- We have a prototype
- {" "}
- that ingests data from Git and GitHub, computes cred, and allows the
- user to explore and experiment on the results. We have a long way to
- go to realize SourceCred’s full vision, but the prototype can already
- surface some interesting insights!
+ We have a prototype that ingests data
+ from Git and GitHub, computes cred, and allows the user to explore and
+ experiment on the results. We have a long way to go to realize
+ SourceCred’s full vision, but the prototype can already surface some
+ interesting insights!
@@ -165,7 +165,7 @@ export default class HomePage extends React.Component<{|+assets: Assets|}> {
In the longer term, we will continue to add signal to cred
attribution. For example, we plan to parse the{" "}
- AST of a project’s code so that we can
+ AST of a project’s code so that we can
attribute cred at the level of individual functions, and create a
“spotlight” mechanic that will let contributors flow more cred to
their peers’ important contributions. As SourceCred improves, we have
@@ -179,30 +179,23 @@ export default class HomePage extends React.Component<{|+assets: Assets|}> {
decentralized. We don’t think communities should have to give their
data to us, or entrust us with control over their cred. The lead
developers are grateful to be supported by{" "}
- Protocol Labs .
+ Protocol Labs.
If you think this vision is exciting, we’d love for you to get
- involved! You can join our Discord and
- check out our GitHub —many of our issues are
- marked contributions welcome .
+ involved! You can join our Discord{" "}
+ and check out our GitHub—many of our
+ issues are marked{" "}
+ contributions welcome.
If you want to try running SourceCred on open-source projects you care
- about, check out our README .
+ about, check out our README.
);
}
}
-function A(props: {|+href: string, +children: Node|}) {
- return (
-
- {props.children}
-
- );
-}
-
function Dt(props) {
return {props.children} ;
}
@@ -210,15 +203,3 @@ function Dt(props) {
function Dd(props) {
return {props.children} ;
}
-
-const styles = StyleSheet.create({
- link: {
- // TODO(@wchargin): Create a ` ` component to share these
- // styles, abstracting over router-links (`to`) and external links
- // (`href`).
- color: "#0872A2",
- ":visited": {
- color: "#084598",
- },
- },
-});
diff --git a/src/app/Link.js b/src/app/Link.js
new file mode 100644
index 0000000..129ee1d
--- /dev/null
+++ b/src/app/Link.js
@@ -0,0 +1,58 @@
+// @flow
+
+import React, {Component} from "react";
+import {Link as RouterLink} from "react-router";
+import {StyleSheet, css} from "aphrodite/no-important";
+
+/**
+ * A styled link component for both client-side router links and normal
+ * external links.
+ *
+ * For a client-side link, specify `to={routePath}`. For a normal anchor
+ * tag, specify `href={href}`.
+ *
+ * To add Aphrodite styles: if you would normally write
+ *
+ *
+ *
+ * then specify `styles={[x, y, z]}`.
+ *
+ * All other properties, including `children`, are forwarded directly.
+ */
+type LinkProps = $ReadOnly<{
+ ...React$ElementConfig<"a">,
+ ...{|+to: string|} | {|+href: string|},
+ +styles?: $ReadOnlyArray<
+ Object | false | null | void
+ > /* Aphrodite styles, as passed to `css` */,
+}>;
+export default class Link extends Component {
+ render() {
+ const linkClass = css(styles.link, this.props.styles);
+ const className = this.props.className
+ ? `${linkClass} ${this.props.className}`
+ : linkClass;
+ const Tag = "to" in this.props ? RouterLink : "a";
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+const colorAttributes = (color) => ({
+ color: color,
+ fill: color, // for child SVGs
+});
+const styles = StyleSheet.create({
+ link: {
+ ...colorAttributes("#0872A2"),
+ ":visited": {
+ ...colorAttributes("#3A066A"),
+ },
+ ":active": {
+ ...colorAttributes("#FF3201"),
+ },
+ },
+});
diff --git a/src/app/Link.test.js b/src/app/Link.test.js
new file mode 100644
index 0000000..33ae496
--- /dev/null
+++ b/src/app/Link.test.js
@@ -0,0 +1,72 @@
+// @flow
+import {StyleSheet} from "aphrodite/no-important";
+import {shallow} from "enzyme";
+import React from "react";
+import {Link as RouterLink} from "react-router";
+
+import Link from "./Link";
+
+require("./testUtil").configureAphrodite();
+require("./testUtil").configureEnzyme();
+
+describe("src/app/Link", () => {
+ const styles = StyleSheet.create({
+ x: {fontWeight: "bold"},
+ });
+
+ // Static type checks
+ void [
+ // Must specify either `href` or `to`
+ click me,
+ click me, too,
+ // $ExpectFlowError
+ missing to/href,
+
+ // May specify styles
+ ,
+
+ // May specify extra properties
+ void alert("hi")} tabIndex={3} />,
+ ];
+
+ it("renders a styled external link", () => {
+ const element = shallow( click me);
+ expect(element.type()).toBe("a");
+ expect(element.prop("href")).toEqual("https://example.com/");
+ expect(element.children().text()).toEqual("click me");
+ expect(typeof element.prop("className")).toBe("string");
+ });
+
+ it("renders a styled router link", () => {
+ const element = shallow( check it out);
+ expect(element.type()).toEqual(RouterLink);
+ expect(element.prop("to")).toEqual("/prototype");
+ expect(element.children().text()).toEqual("check it out");
+ expect(typeof element.prop("className")).toBe("string");
+ });
+
+ it("has deterministic className", () => {
+ const e1 = shallow( );
+ const e2 = shallow( );
+ expect(e2.prop("className")).toEqual(e1.prop("className"));
+ });
+
+ it("adds specified Aphrodite styles", () => {
+ const e1 = shallow( );
+ const e2 = shallow( );
+ expect(e2.prop("className")).not.toEqual(e1.prop("className"));
+ });
+
+ it("forwards class name", () => {
+ const e1 = shallow( );
+ const e2 = shallow( );
+ expect(e2.prop("className")).toEqual(e1.prop("className") + " ohai");
+ });
+
+ it("forwards other props, like `onClick` and `tabIndex`", () => {
+ const fn = () => {};
+ const element = shallow( );
+ expect(element.prop("onClick")).toBe(fn);
+ expect(element.prop("tabIndex")).toBe(77);
+ });
+});
diff --git a/src/app/Page.js b/src/app/Page.js
index a870f5b..bb4a254 100644
--- a/src/app/Page.js
+++ b/src/app/Page.js
@@ -1,10 +1,10 @@
// @flow
import React, {type Node} from "react";
-import {Link} from "react-router";
import {StyleSheet, css} from "aphrodite/no-important";
import type {Assets} from "./assets";
+import Link from "./Link";
import GithubLogo from "./GithubLogo";
import TwitterLogo from "./TwitterLogo";
import DiscordLogo from "./DiscordLogo";
@@ -24,10 +24,7 @@ export default class Page extends React.Component<{|
-
+
SourceCred
@@ -37,44 +34,44 @@ export default class Page extends React.Component<{|
key={path}
className={css(style.navItem, style.navItemRight)}
>
-
+
{navTitle}
))
)}
-
-
+
-
-
+
-
-
+
@@ -131,14 +128,10 @@ const style = StyleSheet.create({
display: "flex",
},
navLink: {
- color: "#0872A2",
- fill: "#0872A2",
fontFamily: "Roboto Condensed",
fontSize: 18,
textDecoration: "none",
":hover": {
- color: "#084598",
- fill: "#084598",
textDecoration: "underline",
},
},
diff --git a/src/app/credExplorer/App.js b/src/app/credExplorer/App.js
index 227a652..9533d2c 100644
--- a/src/app/credExplorer/App.js
+++ b/src/app/credExplorer/App.js
@@ -6,6 +6,7 @@ import type {Assets} from "../assets";
import type {LocalStore} from "../localStore";
import CheckedLocalStore from "../checkedLocalStore";
import BrowserLocalStore from "../browserLocalStore";
+import Link from "../Link";
import {defaultStaticAdapters} from "../adapters/defaultPlugins";
import {PagerankTable} from "./pagerankTable/Table";
@@ -100,15 +101,13 @@ export function createApp(
return (
-
+
what is this?
-
+
{spacer()}
- feedback
+
+ feedback
+
{
it("should have a feedback link with a valid URL", () => {
const {el} = example();
- const link = el.find("a").filterWhere((x) => x.text().includes("feedback"));
+ const link = el.find("Link").filterWhere((x) =>
+ x
+ .children()
+ .text()
+ .includes("feedback")
+ );
expect(link).toHaveLength(1);
expect(link.prop("href")).toMatch(/https?:\/\//);
});
diff --git a/src/plugins/github/__snapshots__/render.test.js.snap b/src/plugins/github/__snapshots__/render.test.js.snap
index fb782e0..4460fab 100644
--- a/src/plugins/github/__snapshots__/render.test.js.snap
+++ b/src/plugins/github/__snapshots__/render.test.js.snap
@@ -3,6 +3,7 @@
exports[`plugins/github/render renders the right description for a comment 1`] = `
Commit
+
{props.children}
-
+
);
}
diff --git a/src/plugins/github/render.test.js b/src/plugins/github/render.test.js
index 2e7da9b..adfd87b 100644
--- a/src/plugins/github/render.test.js
+++ b/src/plugins/github/render.test.js
@@ -5,6 +5,7 @@ import {exampleEntities} from "./example/example";
import {description} from "./render";
import enzymeToJSON from "enzyme-to-json";
+require("../../app/testUtil").configureAphrodite();
require("../../app/testUtil").configureEnzyme();
describe("plugins/github/render", () => {