Merge pull request #936 from collinbarrett/antd2

New UI build on AntDesign
This commit is contained in:
Collin M. Barrett 2019-09-02 20:19:16 +00:00 committed by GitHub
commit 05f680e646
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 1150 additions and 1918 deletions

View file

@ -1,5 +1,3 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
@ -20,4 +18,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*

View file

@ -0,0 +1,9 @@
const { override, fixBabelImports } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
);

Binary file not shown.

View file

@ -1,30 +1,28 @@
{
"name": "web",
"name": "filterlists.web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/jest": "24.0.18",
"@types/node": "12.7.3",
"@types/react": "16.9.2",
"@types/react-dom": "16.9.0",
"@types/jest": "^24.0.18",
"@types/node": "^12.7.3",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^4.3.5",
"@types/react-table": "^6.8.5",
"bootstrap": "^4.3.1",
"es6-promise": "^4.2.8",
"moment": "^2.24.0",
"antd": "^3.22.2",
"babel-plugin-import": "^1.12.1",
"customize-cra": "^0.5.0",
"react": "^16.9.0",
"react-bootstrap": "^1.0.0-beta.12",
"react-app-rewired": "^2.1.3",
"react-dom": "^16.9.0",
"react-router-dom": "^5.0.1",
"react-scripts": "3.1.1",
"react-table": "^6.10.0",
"typescript": "3.6.2"
"react-scripts": "^3.1.1",
"slugify": "^1.3.4",
"typescript": "^3.6.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
},
"proxy": "https://filterlists.com",
"eslintConfig": {
@ -41,6 +39,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {}
}
}
}

View file

@ -1,27 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="shortcut icon"
href="https://filterlists.com/icon_filterlists.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>
FilterLists | Subscriptions for uBlock Origin, Adblock Plus, AdGuard, ...
</title>
<meta
name="description"
content="FilterLists is the independent, comprehensive directory of filter and host lists for advertisements, trackers, malware, and annoyances. By Collin M. Barrett."
/>
<link
rel="icon"
type="image/png"
href="https://filterlists.com/icon_filterlists.png"
/>
<link rel="canonical" href="https://filterlists.com/" />
</head>
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/icon_filterlists.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>FilterLists | Subscriptions for uBlock Origin, Adblock Plus, AdGuard, ...</title>
<meta name="description"
content="FilterLists is the independent, comprehensive directory of filter and host lists for advertisements, trackers, malware, and annoyances. By Collin M. Barrett." />
<link rel="icon" type="image/png" href="icon_filterlists.png">
<link rel="canonical" href="https://filterlists.com/">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<p>FilterLists is built with ReactJS and therefore requires first-party JavaScript to be enabled. We do not use any
third-party JavaScript, and your privacy is very important to us. If you prefer not to enable JavaScript or your
browser does not support it, the data is also available on <a
href="https://github.com/collinbarrett/FilterLists/tree/master/data">our GitHub repo</a>.</p>
</div>
</body>
</html>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<p>
FilterLists is built with ReactJS and therefore requires first-party
JavaScript to be enabled. We do not use any third-party JavaScript, and
your privacy is very important to us. If you prefer not to enable
JavaScript or your browser does not support it, the data is also
available on
<a href="https://github.com/collinbarrett/FilterLists/tree/master/data"
>our GitHub repo</a
>.
</p>
</div>
</body>
</html>

View file

@ -1,14 +0,0 @@
/* use consistent and smaller font size globally */
body,
h2,
h3,
h4,
h5,
blockquote {
font-size: 16px;
}
/* leave a bit of horizontal padding around the app for cleanliness */
.container {
max-width: 90vw;
}

View file

@ -1,9 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { App } from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
});

View file

@ -1,14 +1,90 @@
import { Icon, Layout, Menu } from 'antd';
import React from 'react';
import './App.css';
import { Layout } from "./Layout";
import { Router } from "./Router";
import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
const App: React.FC = () => {
return (
import { ListsTable } from './components';
const { Header, Content, Footer } = Layout;
export const App: React.FC = () =>
<Router>
<Layout>
<Router />
<Header>
<Logo />
<Menu theme="dark" mode="horizontal" style={{ lineHeight: '64px' }} />
</Header>
<Content>
<div style={{ background: '#fff', paddingLeft: 12, paddingTop: 12, paddingRight: 12, minHeight: 280 }}>
<Switch>
<Route path="/" component={ListsTable} />
<Route component={NotFound} />
</Switch>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
<CopyrightAuthor /> | <Twitter /> | <Community /> | <GitHub /> | <Api /> | <Donate />
</Footer>
</Layout>
);
}
</Router>;
export default App;
const Logo = () =>
<img src={`${process.env.PUBLIC_URL}/logo_filterlists.png`}
alt="FilterLists logo"
height="44px"
style={{ background: '#fff' }} />;
const NotFound = (props: RouteComponentProps) =>
<h2>
404 Not Found: <code>{props.location.pathname}</code>
</h2>;
const CopyrightAuthor = () =>
<>
©{new Date().getFullYear()}&nbsp;
<a href="https://collinmbarrett.com"
title="Collin M. Barrett's Homepage"
target="_blank"
rel="noopener noreferrer">
Collin M. Barrett
</a>
</>;
const Twitter = () =>
<a href="https://twitter.com/FilterLists"
title="FilterLists on Twitter"
target="_blank"
rel="noopener noreferrer">
<Icon type="twitter" /> Twitter
</a>;
const Community = () =>
<a href="https://hub.filterlists.com"
title="FilterLists Discourse community forum"
target="_blank"
rel="noopener noreferrer">
<Icon type="team" /> Community
</a>;
const GitHub = () =>
<a href="https://github.com/collinbarrett/FilterLists"
title="FilterLists git repository on GitHub"
target="_blank"
rel="noopener noreferrer">
<Icon type="github" /> GitHub
</a>;
const Api = () =>
<a href="/api/v1/lists"
title="FilterLists API lists endpoint"
target="_blank"
rel="noopener noreferrer">
<Icon type="api" /> API
</a>;
const Donate = () =>
<a href="https://beerpay.io/collinbarrett/FilterLists"
title="Donate to FilterLists with Beerpay"
target="_blank"
rel="noopener noreferrer">
<Icon type="dollar" /> Donate
</a>;

View file

@ -1,60 +0,0 @@
import * as React from "react";
interface Props {
children?: React.ReactNode;
};
export const Layout = (props: Props) =>
<div className="container">
<Header />
<div className="row">
<div className="w-100">
{props.children}
</div>
</div>
<Footer />
</div>;
const Header = () =>
<header className="row">
<h1>
<a href="./">
<img src="logo_filterlists.png" alt="FilterLists" className="img-fluid" style={{ maxWidth: "60%" }}/>
</a>
</h1>
</header>;
const Footer = () =>
<footer className="row justify-content-center">
<p className="mt-2 ml-1 mr-1">
<HubLink /> | <GitHubLink /> | <ApiLink /> | <DonateLink /> | By <OwnerLink />
</p>
</footer>;
const HubLink = () =>
<a href="https://hub.filterlists.com"
title="Discourse community forum">
Hub
</a>;
const GitHubLink = () =>
<a href="https://github.com/collinbarrett/FilterLists">
GitHub
</a>;
const ApiLink = () =>
<a href="/api/v1/lists"
title="API lists endpoint">
API
</a>;
const DonateLink = () =>
<a href="https://beerpay.io/collinbarrett/FilterLists"
title="Support with Beerpay">
Donate
</a>;
const OwnerLink = () =>
<a href="https://collinmbarrett.com/">
Collin M. Barrett
</a>;

View file

@ -1,13 +0,0 @@
import * as React from "react";
import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom";
import { Home } from "./modules";
const Routes =
<Switch>
<Route exact path="/" component={Home} />
<Route>
<Redirect to="/" />
</Route>
</Switch>;
export const Router = () => <BrowserRouter children={Routes} />

View file

@ -0,0 +1,11 @@
import React from 'react';
interface Props {
description: string;
descriptionSourceUrl: string;
};
export const Description = (props: Props) =>
props.description
? <blockquote cite={props.descriptionSourceUrl}>{props.description}</blockquote>
: <p>{props.description}</p>;

View file

@ -0,0 +1,31 @@
import { Tag } from 'antd';
import * as React from 'react';
interface Props {
name: string;
descriptionUrl: string;
};
export const LicenseTag = (props: Props) =>
props.name
? <>
<h3>License:</h3>
<Tag>
<TagContents
name={props.name}
descriptionUrl={props.descriptionUrl} />
</Tag>
</>
: null;
const TagContents = (props: Props) =>
props.descriptionUrl
? <a href={props.descriptionUrl}
title={`View ${props.name}'s homepage.`}
target="_blank"
rel="noopener noreferrer">
{props.name}
</a>
: <>
{props.name}
</>;

View file

@ -0,0 +1,26 @@
import { Button } from 'antd';
import { ButtonType } from 'antd/lib/button';
import React from 'react';
interface Props {
url: string;
text: string;
title?: string;
type?: ButtonType;
icon?: string;
};
export const LinkButton = (props: Props) =>
props.url && props.text
? <Button
href={props.url}
title={props.title}
type={props.type || "default"}
block
style={{ borderLeftColor: "rgb(217, 217, 217)" }} //HACK: override buggy style in antd
icon={props.icon}
target="_blank"
rel="noopener noreferrer" >
{props.text}
</Button >
: null;

View file

@ -0,0 +1,22 @@
import { Button, Icon } from 'antd';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import slugify from 'slugify';
interface Props {
listName: string;
};
export const ListInfoButton = (props: RouteComponentProps & Props) => {
const listPath = `/lists/${slugify(props.listName)}`;
return (
<Button
type="primary"
title={`View more information about ${props.listName}.`}
onClick={() => props.location.pathname === listPath
? props.history.push("/")
: props.history.push(listPath)} >
<Icon type="info-circle" />
</Button>
);
};

View file

@ -0,0 +1,122 @@
import { Divider, Drawer } from 'antd';
import ButtonGroup from 'antd/lib/button/button-group';
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Language } from '../interfaces/Language';
import { License } from '../interfaces/License';
import { List } from '../interfaces/List';
import { Software } from '../interfaces/Software';
import { Syntax } from '../interfaces/Syntax';
import { Tag } from '../interfaces/Tag';
import { Description } from './Description';
import { LanguageCloud } from './languageCloud';
import { LicenseTag } from './LicenseTag';
import { LinkButton } from './LinkButton';
import { PublishedDate } from './PublishedDate';
import { SoftwareCloud } from './softwareCloud';
import { SubscribeButtons } from './SubscribeButtons';
import { SyntaxTag } from './SyntaxTag';
import { TagCloud } from './tagCloud';
interface Props {
list: List;
languages: Language[];
license: License | undefined;
software: Software[];
syntax: Syntax | undefined;
tags: Tag[];
};
export const ListInfoDrawer = (props: RouteComponentProps & Props) => {
const [originalTitle] = useState<string>(document.title);
useEffect(() => {
const updateTitle = () => document.title = props.list.name + " | " + originalTitle;
updateTitle();
return () => { document.title = originalTitle; };
}, [props.list, originalTitle]);
return (
<Drawer
visible={true}
width={350}
placement={"right"}
mask={false}
title={props.list.name}
destroyOnClose={true}
onClose={() => props.history.push("/")}>
<Description
description={props.list.description}
descriptionSourceUrl={props.list.descriptionSourceUrl} />
<LanguageCloud
languages={props.languages}
showLabel={true} />
<TagCloud
tags={props.tags}
showLabel={true} />
{props.license &&
<LicenseTag
name={props.license.name}
descriptionUrl={props.license.descriptionUrl} />}
{props.syntax &&
<SyntaxTag
name={props.syntax.name}
definitionUrl={props.syntax.definitionUrl} />}
<SoftwareCloud
software={props.software}
showLabel={true} />
<PublishedDate publishedDate={props.list.publishedDate} />
<Divider />
<ButtonGroup style={{ display: "inherit" }}>
<SubscribeButtons
name={props.list.name}
viewUrl={props.list.viewUrl}
viewUrlMirrors={props.list.viewUrlMirrors} />
<LinkButton
url={props.list.viewUrl}
text="View"
title={`View ${props.list.name} in its raw format`}
icon="search" />
<LinkButton
url={props.list.homeUrl}
text="Home"
title={`View ${props.list.name}'s homepage.`}
icon="home" />
<LinkButton
url={props.list.policyUrl}
text="Policy"
title={`View the types of rules that ${props.list.name} includes.`}
icon="file-exclamation" />
<LinkButton
url={`mailto:${props.list.emailAddress}`}
text="Email"
title={`Email ${props.list.name}.`}
icon="mail" />
<LinkButton
url={props.list.issuesUrl}
text="GitHub Issues"
title={`View the GitHub Issues for ${props.list.name}.`}
icon="github" />
<LinkButton
url={props.list.submissionUrl}
text="Submit a Rule"
title={`Submit a new rule to be included in ${props.list.name}.`}
icon="form" />
<LinkButton
url={props.list.forumUrl}
text="Forum"
title={`View the forum for ${props.list.name}.`}
icon="team" />
<LinkButton
url={props.list.chatUrl}
text="Chat"
title={`Enter the chat room for ${props.list.name}.`}
icon="message" />
<LinkButton
url={props.list.donateUrl}
text="Donate"
title={`Donate to the maintainer of ${props.list.name}.`}
icon="dollar" />
</ButtonGroup>
</Drawer>
);
};

View file

@ -0,0 +1,13 @@
import React from 'react';
interface Props {
publishedDate: string;
};
export const PublishedDate = (props: Props) =>
props.publishedDate
? <>
<h3>First Published Date:</h3>
{new Date(props.publishedDate).toDateString()}
</>
: null;

View file

@ -0,0 +1,95 @@
import { Button } from 'antd';
import { ButtonType } from 'antd/lib/button';
import React from 'react';
interface Props {
name: string;
viewUrl: string;
viewUrlMirrors: string[];
};
export const SubscribeButtons = (props: Props) =>
props.viewUrl
? <>
<SubscribeButton
name={props.name}
viewUrl={props.viewUrl}
text="Subscribe" />
<MirrorButtons
name={props.name}
viewUrlMirrors={props.viewUrlMirrors} />
</>
: null;
interface MirrorButtonsProps {
name: string;
viewUrlMirrors: string[];
};
const MirrorButtons = (props: MirrorButtonsProps) =>
<>
{props.viewUrlMirrors && props.viewUrlMirrors.length
? props.viewUrlMirrors.map((viewUrlMirror: string, i: number) =>
<SubscribeButton
key={i}
name={props.name}
viewUrl={viewUrlMirror}
text={`Subscribe (Mirror ${i + 1})`} />)
: null}
</>;
interface SubscribeButtonProps {
name: string;
viewUrl: string;
text: string;
};
const SubscribeButton = (props: SubscribeButtonProps) => {
const buttonProps = buildButtonProps(props.name, props.viewUrl);
return (
<Button
disabled={buttonProps.disabled}
block
icon="import"
type={buttonProps.type}
href={buttonProps.href}
title={buttonProps.title}>
{props.text}
</Button>
);
};
const buildButtonProps = (name: string, viewUrl: string) => {
let type: ButtonType = "primary";
let disabled: boolean = false;
const hrefLocation = `${encodeURIComponent(viewUrl)}`;
const hrefTitle = `${encodeURIComponent(name)}`;
let href = `abp:subscribe?location=${hrefLocation}&amp;title=${hrefTitle}`;
let prefixes: string[] = [];
let message = `Subscribe to ${name} with a browser extension supporting the "abp:" protocol (e.g. uBlock Origin, Adblock Plus).`;
// HTTP protocols
if (viewUrl.includes(".onion/")) {
type = "dashed";
prefixes.push("TOR");
};
if (viewUrl.includes("http://")) {
type = "danger";
prefixes.push("INSECURE");
};
// Software protocols
if (viewUrl.includes(".tpl")) {
disabled = true; // IE not supported by FilterLists
};
if (viewUrl.includes(".lsrules") || viewUrl.includes("?hostformat=littlesnitch")) {
href = `x-littlesnitch:subscribe-rules?url=${hrefLocation}`;
message = `Subscribe to ${name} with Little Snitch's rule group subscription feature.`;
};
const title = `${prefixes.length ? prefixes.join(" | ") + " | " : ""}${message}`;
return { disabled, type, href, title };
};

View file

@ -0,0 +1,31 @@
import { Tag } from 'antd';
import * as React from 'react';
interface Props {
name: string;
definitionUrl: string;
};
export const SyntaxTag = (props: Props) =>
props.name
? <>
<h3>Syntax:</h3>
<Tag>
<TagContents
name={props.name}
definitionUrl={props.definitionUrl} />
</Tag>
</>
: null;
const TagContents = (props: Props) =>
props.definitionUrl
? <a href={props.definitionUrl}
title={`View ${props.name}'s homepage.`}
target="_blank"
rel="noopener noreferrer">
{props.name}
</a>
: <>
{props.name}
</>;

View file

@ -0,0 +1,3 @@
import { ListsTable } from './listsTable';
export { ListsTable };

View file

@ -0,0 +1,4 @@
/* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */
.grow {
max-height: none !important;
}

View file

@ -0,0 +1,19 @@
import { Tag } from 'antd';
import React from 'react';
import { Language } from '../../interfaces/Language';
import styles from './LanguageCloud.module.css';
interface Props {
languages: Language[];
showLabel?: boolean;
};
export const LanguageCloud = (props: Props) =>
props.languages && props.languages.length
? <div className={styles.grow}>
{props.showLabel && <h3>{`Language${props.languages.length > 1 ? "s" : ""}:`}</h3>}
{props.languages.map((l: Language, i: number) =>
<Tag key={i} title={l.name}>{l.iso6391}</Tag>)}
</div>
: null;

View file

@ -0,0 +1,3 @@
import { LanguageCloud } from './LanguageCloud';
export { LanguageCloud };

View file

@ -0,0 +1,36 @@
import React from 'react';
import { Redirect, Route, RouteComponentProps, StaticContext } from 'react-router';
import { Language } from '../../interfaces/Language';
import { License } from '../../interfaces/License';
import { List } from '../../interfaces/List';
import { Software } from '../../interfaces/Software';
import { Syntax } from '../../interfaces/Syntax';
import { Tag } from '../../interfaces/Tag';
import { ListInfoDrawer } from '../ListInfoDrawer';
interface Props {
lists: List[];
languages: Language[];
licenses: License[];
software: Software[];
syntaxes: Syntax[];
tags: Tag[];
};
export const ListDrawer = (props: Props) => {
const renderDrawer = (rp: RouteComponentProps<any, StaticContext, any>) => {
const list = props.lists.find(l => l.slug === rp.match.params.listSlug);
return list
? <ListInfoDrawer
list={list as List}
languages={list.languageIds && props.languages.filter((l: Language) => list.languageIds.includes(l.id))}
license={list.licenseId ? props.licenses.find((l: License) => list.licenseId === l.id) : props.licenses.find((l: License) => l.id === 5)}
software={list.syntaxId ? props.software.filter((s: Software) => s.syntaxIds.includes(list.syntaxId)) : []}
syntax={list.syntaxId ? props.syntaxes.find(s => s.id === list.syntaxId) : undefined}
tags={list.tagIds && props.tags.filter((t: Tag) => list.tagIds.includes(t.id))}
{...rp} />
: (props.lists && props.lists.length && <Redirect to={{ pathname: "/", }} />) || null;
};
return <Route path="/lists/:listSlug" render={renderDrawer} />;
};

View file

@ -0,0 +1,21 @@
.nogrow > * {
/* limit table cells from wrapping more than 2 lines */
/* https://stackoverflow.com/a/13924997/2343739 */
/* https://stackoverflow.com/a/22042054/2343739 */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box !important;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* number of lines to show */
line-height: 1.5; /* fallback */
max-height: 3em; /* fallback */
/* fixed row height */
min-height: 3em;
}
.nogrow {
/* eliminate top and bottom padding in cells to fit more rows per page */
padding-top: 0px !important;
padding-bottom: 0px !important;
}

View file

@ -0,0 +1,145 @@
import { Table, Tag } from 'antd';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { useSearchColumnFilter, useTablePageSizer } from '../../hooks';
import { Language } from '../../interfaces/Language';
import { License } from '../../interfaces/License';
import { List } from '../../interfaces/List';
import { Software } from '../../interfaces/Software';
import { Tag as TagInterface } from '../../interfaces/Tag';
import { nameof } from '../../utils';
import { Description } from '../Description';
import { LanguageCloud } from '../languageCloud';
import { ListInfoButton } from '../ListInfoButton';
import { SoftwareCloud, SoftwareIcon } from '../softwareCloud';
import { TagCloud } from '../tagCloud';
import { arraySorter } from './arraySorter';
import styles from './ListsTable.module.css';
interface Props {
lists: List[];
languages: Language[];
licenses: License[];
software: Software[];
tags: TagInterface[];
};
export const ListsTable = (props: RouteComponentProps & Props) => {
const { lists, languages, licenses, software, tags, ...routeComponentProps } = props;
const tablePageSize = useTablePageSizer();
const searchNameColumn = useSearchColumnFilter<List>(nameof<List>("name"));
const searchDescriptionColumn = useSearchColumnFilter<List>(nameof<List>("description"));
return (
<Table<List>
dataSource={lists}
rowKey={record => record.id.toString()}
loading={lists.length ? false : true}
size="small"
pagination={{
size: "small",
simple: true,
style: { float: "left" },
pageSize: tablePageSize.pageSize
}}
scroll={{ x: tablePageSize.isNarrowWindow ? undefined : 1200 }}>
<Table.Column<List>
title="Info"
dataIndex={nameof<List>("id")}
className={styles.nogrow}
fixed="left"
render={(_id: number, list: List) =>
<ListInfoButton listName={list.name} {...routeComponentProps} />} />
<Table.Column<List>
title="Name"
dataIndex={nameof<List>("name")}
sorter={(a, b) => a.name.localeCompare(b.name)}
defaultSortOrder={"ascend"}
width={tablePageSize.isNarrowWindow ? undefined : 200}
className={styles.nogrow}
fixed="left"
filterDropdown={searchNameColumn.filterDropdown}
filterIcon={searchNameColumn.filterIcon}
onFilter={searchNameColumn.onFilter}
render={(name: string) =>
<div>{name}</div>} />
{tablePageSize.isNarrowWindow
? null
: <Table.Column<List>
title="Description"
dataIndex={nameof<List>("description")}
className={styles.nogrow}
filterDropdown={searchDescriptionColumn.filterDropdown}
filterIcon={searchDescriptionColumn.filterIcon}
onFilter={searchDescriptionColumn.onFilter}
render={(description: string, list: List) =>
<Description
description={description}
descriptionSourceUrl={list.descriptionSourceUrl} />} />}
{tablePageSize.isNarrowWindow
? null
: <Table.Column<List>
title="Software"
dataIndex={nameof<List>("syntaxId")}
sorter={(a, b) => {
const getSoftwareIds = (l: List) => software.filter((s: Software) => s.syntaxIds.includes(l.syntaxId)).map(s => s.id);
return arraySorter(getSoftwareIds(a), getSoftwareIds(b), software);
}}
width={143}
className={styles.nogrow}
filters={software.map(s => ({
text: <>
<SoftwareIcon id={s.id} />&nbsp;
{s.name}
</>,
value: s.name
}))}
onFilter={(value, record) => software.filter((s: Software) => s.name === value).flatMap(s => s.syntaxIds).includes(record.syntaxId)}
render={(syntaxId: number) =>
syntaxId
? <SoftwareCloud software={software.filter((s: Software) => s.syntaxIds.includes(syntaxId))} />
: null} />}
{tablePageSize.isNarrowWindow
? null
: <Table.Column<List>
title="Languages"
dataIndex={nameof<List>("languageIds")}
sorter={(a, b) => arraySorter(a.languageIds, b.languageIds, languages)}
width={125}
className={styles.nogrow}
filters={languages.map(l => ({
text: <>
<Tag title={l.name}>{l.iso6391}</Tag>&nbsp;
{l.name}
</>,
value: l.id.toString()
}))}
onFilter={(value, record) => record.languageIds
? record.languageIds.includes(+value)
: false}
render={(languageIds: number[]) =>
languageIds
? <LanguageCloud languages={languages.filter((l: Language) => languageIds.includes(l.id))} />
: null} />}
{tablePageSize.isNarrowWindow
? null
: <Table.Column<List>
title="Tags"
dataIndex={nameof<List>("tagIds")}
sorter={(a, b) => arraySorter(a.tagIds, b.tagIds, tags)}
width={275}
className={styles.nogrow}
filters={tags.map(t => ({
text: <Tag title={t.description}>{t.name}</Tag>,
value: t.id.toString()
}))}
onFilter={(value, record) => record.tagIds
? record.tagIds.includes(+value)
: false}
render={(tagIds: number[]) =>
tagIds
? <TagCloud tags={tags.filter((t: TagInterface) => tagIds.includes(t.id))} />
: null} />}
</Table>
);
};

View file

@ -0,0 +1,33 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useLanguages, useLicenses, useLists, useSoftware, useSyntaxes, useTags } from '../../hooks';
import { ListDrawer } from './ListDrawer';
import { ListsTable } from './ListsTable';
export const ListsTableHoc = (props: RouteComponentProps) => {
const lists = useLists();
const languages = useLanguages();
const licenses = useLicenses();
const software = useSoftware();
const syntaxes = useSyntaxes();
const tags = useTags();
return (
<>
<ListsTable
lists={lists}
languages={languages}
licenses={licenses}
software={software}
tags={tags}
{...props} />
<ListDrawer
lists={lists}
languages={languages}
licenses={licenses}
software={software}
syntaxes={syntaxes}
tags={tags} />
</>
);
};

View file

@ -0,0 +1,18 @@
interface ArraySortableEntity {
id: number;
name: string;
};
export const arraySorter = (a: number[], b: number[], entities: ArraySortableEntity[]) =>
a && a.length
? b && b.length
? a.length === b.length
? entities.filter((e: ArraySortableEntity) => a.includes(e.id)).map((e: ArraySortableEntity) => e.name).join().toLowerCase()
> entities.filter((e: ArraySortableEntity) => b.includes(e.id)).map((e: ArraySortableEntity) => e.name).join().toLowerCase()
? 1
: -1
: a.length > b.length
? -1
: 1
: -1
: 1;

View file

@ -0,0 +1,3 @@
import { ListsTableHoc } from './ListsTableHoc';
export { ListsTableHoc as ListsTable };

View file

@ -0,0 +1,4 @@
/* allow for 2nd row's bottom border to not be chopped in AllListsTable */
.grow {
max-height: none !important;
}

View file

@ -0,0 +1,26 @@
import React from 'react';
import { Software } from '../../interfaces/Software';
import styles from './SoftwareCloud.module.css';
import { SoftwareIcon } from './SoftwareIcon';
interface Props {
software: Software[];
showLabel?: boolean;
};
export const SoftwareCloud = (props: Props) =>
props.software && props.software.length
? <div className={styles.grow}>
{props.showLabel && <h3>{`Software:`}</h3>}
{props.software.map((s: Software, i: number) =>
s.homeUrl
? <a key={i}
href={s.homeUrl}
target="_blank"
rel="noopener noreferrer">
<SoftwareIcon id={s.id} />
</a>
: <SoftwareIcon key={i} id={s.id} />)}
</div>
: null;

View file

@ -1,5 +1,43 @@
import * as React from "react";
import { img1, img10, img11, img12, img13, img14, img15, img16, img17, img18, img19, img2, img20, img21, img22, img23, img24, img25, img26, img27, img28, img29, img3, img30, img31, img32, img33, img34, img35, img36, img37, img4, img5, img6, img7, img8 } from "./imgs";
import * as React from 'react';
import {
img01,
img02,
img03,
img04,
img05,
img06,
img07,
img08,
img10,
img11,
img12,
img13,
img14,
img15,
img16,
img17,
img18,
img19,
img20,
img21,
img22,
img23,
img24,
img25,
img26,
img27,
img28,
img29,
img30,
img31,
img32,
img33,
img34,
img35,
img36,
img37
} from './imgs';
interface Props {
id: number;
@ -7,26 +45,27 @@ interface Props {
export const SoftwareIcon = (props: Props) =>
icons[props.id]
? <img src={icons[props.id].image}
? <img
src={icons[props.id].image}
height="20"
alt={icons[props.id].imageTitle}
title={icons[props.id].imageTitle} />
title={`View ${icons[props.id].imageTitle}'s homepage.`} />
: null;
interface IIcon {
interface Icon {
image: any;
imageTitle: string;
};
const icons: { [id: number]: IIcon; } = {
1: { image: img1, imageTitle: "uBlock Origin" },
2: { image: img2, imageTitle: "Adblock Plus" },
3: { image: img3, imageTitle: "AdGuard" },
4: { image: img4, imageTitle: "DNS66" },
5: { image: img5, imageTitle: "Nano Adblocker" },
6: { image: img6, imageTitle: "AdBlock" },
7: { image: img7, imageTitle: "AdAway" },
8: { image: img8, imageTitle: "Personal Blocklist" },
const icons: { [id: number]: Icon; } = {
1: { image: img01, imageTitle: "uBlock Origin" },
2: { image: img02, imageTitle: "Adblock Plus" },
3: { image: img03, imageTitle: "AdGuard" },
4: { image: img04, imageTitle: "DNS66" },
5: { image: img05, imageTitle: "Nano Adblocker" },
6: { image: img06, imageTitle: "AdBlock" },
7: { image: img07, imageTitle: "AdAway" },
8: { image: img08, imageTitle: "Personal Blocklist" },
10: { image: img10, imageTitle: "Redirector" },
11: { image: img11, imageTitle: "Hosts File Editor" },
12: { image: img12, imageTitle: "Gas Mask" },
@ -54,5 +93,5 @@ const icons: { [id: number]: IIcon; } = {
34: { image: img34, imageTitle: "Windows command line" },
35: { image: img35, imageTitle: "Shadowsocks" },
36: { image: img36, imageTitle: "ShadowsocksR" },
37: { image: img37, imageTitle: "Shadowrocket" },
};
37: { image: img37, imageTitle: "Shadowrocket" }
};

View file

@ -0,0 +1,75 @@
import img01 from './01-uBlock-Origin.svg';
import img02 from './02-Adblock-Plus.svg';
import img03 from './03-AdGuard.svg';
import img04 from './04-DNS66.png';
import img05 from './05-Nano-Adblocker.png';
import img06 from './06-AdBlock.png';
import img07 from './07-AdAway.png';
import img08 from './08-Personal-Blocklist.png';
import img10 from './10-Redirector.png';
import img11 from './11-Hosts-File-Editor.png';
import img12 from './12-Gas-Mask.png';
import img13 from './13-MinerBlock.svg';
import img14 from './14-Pi-hole.svg';
import img15 from './15-uBlock.svg';
import img16 from './16-Internet-Explorer-TPL.svg';
import img17 from './17-Google-Hit-Hider-by-Domain.png';
import img18 from './18-FireHOL.png';
import img19 from './19-Samsung-Knox.png';
import img20 from './20-Little-Snitch.png';
import img21 from './21-Privoxy.png';
import img22 from './22-Diversion.png';
import img23 from './23-dnsmasq.png';
import img24 from './24-Slimjet.png';
import img25 from './25-uMatrix.png';
import img26 from './26-Blokada.png';
import img27 from './27-hostsmgr.png';
import img28 from './28-personalDNSfilter.svg';
import img29 from './29-Unbound.png';
import img30 from './30-BIND.png';
import img31 from './31-AdGuard-Home.png';
import img32 from './32-AdNauseam.png';
import img33 from './33-Legacy-Unix-Derivatives.png';
import img34 from './34-Windows-command-line.png';
import img35 from './35-Shadowsocks.png';
import img36 from './36-ShadowsocksR.png';
import img37 from './37-Shadowrocket.png';
export {
img01,
img02,
img03,
img04,
img05,
img06,
img07,
img08,
img10,
img11,
img12,
img13,
img14,
img15,
img16,
img17,
img18,
img19,
img20,
img21,
img22,
img23,
img24,
img25,
img26,
img27,
img28,
img29,
img30,
img31,
img32,
img33,
img34,
img35,
img36,
img37
};

View file

@ -0,0 +1,4 @@
import { SoftwareCloud } from './SoftwareCloud';
import { SoftwareIcon } from './SoftwareIcon';
export { SoftwareCloud, SoftwareIcon };

View file

@ -0,0 +1,4 @@
/* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */
.grow {
max-height: none !important;
}

View file

@ -0,0 +1,19 @@
import { Tag } from 'antd';
import React from 'react';
import { Tag as TagInterface } from '../../interfaces/Tag';
import styles from './TagCloud.module.css';
interface Props {
tags: TagInterface[];
showLabel?: boolean;
};
export const TagCloud = (props: Props) =>
props.tags && props.tags.length
? <div className={styles.grow}>
{props.showLabel && <h3>{`Tag${props.tags.length > 1 ? "s" : ""}:`}</h3>}
{props.tags.map((t: TagInterface, i: number) =>
<Tag key={i} title={t.description}>{t.name}</Tag>)}
</div>
: null;

View file

@ -0,0 +1,3 @@
import { TagCloud } from './TagCloud';
export { TagCloud };

View file

@ -0,0 +1,19 @@
import { useLanguages } from './useLanguages';
import { useLicenses } from './useLicenses';
import { useLists } from './useLists';
import { useSearchColumnFilter } from './useSearchColumnFilter';
import { useSoftware } from './useSoftware';
import { useSyntaxes } from './useSyntaxes';
import { useTablePageSizer } from './useTablePageSizer';
import { useTags } from './useTags';
export {
useLanguages,
useLicenses,
useLists,
useSearchColumnFilter,
useSoftware,
useSyntaxes,
useTablePageSizer,
useTags
};

View file

@ -0,0 +1,10 @@
import { useEffect, useState } from 'react';
export const useApiData = <T extends {}>(url: string) => {
const [data, setData] = useState<T>();
useEffect(() => {
const fetchData = async () => (await fetch(url)).json().then(r => setData(r));
fetchData();
}, [url]);
return data;
};

View file

@ -0,0 +1,5 @@
import { Language } from '../interfaces/Language';
import { useApiData } from './useApiData';
export const useLanguages = () => (useApiData<Language[]>("/api/v1/languages") || [])
.sort((a: Language, b: Language) => a.name.localeCompare(b.name));

View file

@ -0,0 +1,5 @@
import { License } from '../interfaces/License';
import { useApiData } from './useApiData';
export const useLicenses = () => (useApiData<License[]>("/api/v1/licenses") || [])
.sort((a: License, b: License) => a.name.localeCompare(b.name));

View file

@ -0,0 +1,11 @@
import slugify from 'slugify';
import { List } from '../interfaces/List';
import { useApiData } from './useApiData';
export const useLists = () => {
const lists = useApiData<List[]>("/api/v1/lists");
lists && lists.forEach(l => l.slug = slugify(l.name));
return (lists || [])
.sort((a: List, b: List) => a.name.localeCompare(b.name));
};

View file

@ -0,0 +1,57 @@
import { Button, Icon, Input } from 'antd';
import { FilterDropdownProps } from 'antd/lib/table';
import React, { useEffect, useState } from 'react';
interface FilterPropsState<T> {
filterDropdown?: React.ReactNode | ((props: FilterDropdownProps) => React.ReactNode);
filterIcon?: React.ReactNode | ((filtered: boolean) => React.ReactNode);
onFilter?: (value: any, record: T) => boolean;
}
export const useSearchColumnFilter = <T extends {}>(dataIndex: string) => {
const [filterProps, setFilterProps] = useState<FilterPropsState<T>>({
filterDropdown: undefined,
filterIcon: undefined,
onFilter: undefined
});
useEffect(() => {
const handleSearch = (confirm?: () => void) => {
confirm && confirm();
};
const handleReset = (clearFilters?: (selectedKeys: string[]) => void) => {
clearFilters && clearFilters([]);
};
setFilterProps({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
placeholder={`Search ${dataIndex.charAt(0).toUpperCase() + dataIndex.slice(1)}`}
value={selectedKeys && selectedKeys[0]}
onChange={e => setSelectedKeys && setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => handleSearch(confirm)}
style={{ width: 188, marginBottom: 8, display: 'block' }} />
<Button
type="primary"
onClick={() => handleSearch(confirm)}
icon="search"
size="small"
style={{ width: 90, marginRight: 8 }} >
Search
</Button>
<Button
onClick={() => handleReset(clearFilters)}
size="small"
style={{ width: 90 }} >
Reset
</Button>
</div>
),
filterIcon: filtered => <Icon type="search" style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) => {
const searchValue = (record as any)[dataIndex];
return searchValue && searchValue.toString().toLowerCase().includes(value.toString().toLowerCase())
}
})
}, [dataIndex]);
return filterProps;
};

View file

@ -0,0 +1,5 @@
import { Software } from '../interfaces/Software';
import { useApiData } from './useApiData';
export const useSoftware = () => (useApiData<Software[]>("/api/v1/software") || [])
.sort((a: Software, b: Software) => a.name.localeCompare(b.name));

View file

@ -0,0 +1,5 @@
import { Syntax } from '../interfaces/Syntax';
import { useApiData } from './useApiData';
export const useSyntaxes = () => (useApiData<Syntax[]>("/api/v1/syntaxes") || [])
.sort((a: Syntax, b: Syntax) => a.name.localeCompare(b.name));

View file

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
interface TablePageSize {
pageSize: number;
isNarrowWindow: boolean;
};
export const useTablePageSizer = () => {
const [state, setState] = useState<TablePageSize>(calculateSize());
useEffect(() => {
const updatePageSize = () => setState(calculateSize());
window.addEventListener('resize', updatePageSize);
return () => window.removeEventListener('resize', updatePageSize);
}, []);
return state;
};
const calculateSize = () => ({
pageSize: Math.floor((window.innerHeight - 211.5) / 56),
isNarrowWindow: window.innerWidth < 576 ? true : false
});

View file

@ -0,0 +1,5 @@
import { Tag } from '../interfaces/Tag';
import { useApiData } from './useApiData';
export const useTags = () => (useApiData<Tag[]>("/api/v1/tags") || [])
.sort((a: Tag, b: Tag) => a.name.localeCompare(b.name));

View file

@ -1,7 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import App from './App';
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App />, document.getElementById('root'));

View file

@ -1,4 +1,4 @@
export interface Language {
export interface Language {
id: number;
filterListIds: number[];
iso6391: string;

View file

@ -1,4 +1,4 @@
export interface ILicense {
export interface License {
id: number;
descriptionUrl: string;
filterListIds: number[];

View file

@ -1,4 +1,4 @@
export interface IList {
export interface List {
id: number;
chatUrl: string;
description: string;
@ -19,4 +19,7 @@
tagIds: number[];
viewUrl: string;
viewUrlMirrors: string[];
// auto-generated by useLists hook
slug: string;
};

View file

@ -1,4 +1,4 @@
export interface ISoftware {
export interface Software {
id: number;
homeUrl: string;
isAbpSubscribable: boolean;

View file

@ -1,4 +1,4 @@
export interface ISyntax {
export interface Syntax {
id: number;
definitionUrl: string;
filterListIds: number[];

View file

@ -0,0 +1,6 @@
export interface Tag {
id: number;
description: string;
filterListIds: number[];
name: string;
};

View file

@ -1,111 +0,0 @@
import * as React from "react";
import { ListsTable, Oneliner } from "./components";
import { IColumnVisibility } from "./interfaces/IColumnVisibility";
import { ILicense } from "./interfaces/ILicense";
import { IList } from "./interfaces/IList";
import { IMaintainer } from "./interfaces/IMaintainer";
import { ISoftware } from "./interfaces/ISoftware";
import { ISyntax } from "./interfaces/ISyntax";
import { ITag } from "./interfaces/ITag";
import { Language } from "./interfaces/Language";
const columnVisibilityDefaults: IColumnVisibility[] = [
{ column: "Software", visible: true },
{ column: "Languages", visible: true },
{ column: "Tags", visible: true },
{ column: "License", visible: false },
{ column: "Maintainers", visible: false },
{ column: "Subscribe", visible: false }
];
interface Props {
languages: Language[];
licenses: ILicense[];
lists: IList[];
maintainers: IMaintainer[];
software: ISoftware[];
syntaxes: ISyntax[];
tags: ITag[];
};
interface IState {
columnVisibility: IColumnVisibility[];
pageSize: number;
};
export class Home extends React.Component<Props, IState> {
constructor(props: Props) {
super(props);
this.state = {
columnVisibility: columnVisibilityDefaults,
pageSize: 20
};
this.updatePageSize = this.updatePageSize.bind(this);
}
componentDidMount() {
this.setMobileColumnVisibility();
this.updatePageSize();
};
setMobileColumnVisibility() {
if (window.innerWidth < 768) {
this.state.columnVisibility.forEach((c: IColumnVisibility) => {
c.visible = false;
});
}
};
updatePageSize() {
this.setState({
pageSize: Math.max(Math.floor((window.innerHeight - 340) / 55), 5)
});
};
render() {
return <div>
<Oneliner listCount={this.props.lists.length} />
<ListsTable {...this.props} {...this.state} />
{this.renderColumnVisibilityCheckboxes()}
</div>;
};
renderColumnVisibilityCheckboxes() {
return this.props.lists.length > 0
? <div className="d-none d-md-block text-right">
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map(
(c: IColumnVisibility, i: number) => this.renderColumnVisibilityCheckbox(c, i))}
</div>
: null;
};
renderColumnVisibilityCheckbox(props: IColumnVisibility, key: number) {
return <div className="form-check form-check-inline" key={key}>
<input className="form-check-input"
type="checkbox"
id={`checkbox${props.column.replace(/\s+/g, "")}`}
defaultChecked={props.visible}
onChange={() => this.checkColumn(props)} />
<label className="form-check-label"
htmlFor={`checkbox${props.column.replace(/\s+/g, "")}`}>
{props.column}
</label>
</div>;
};
checkColumn(props: IColumnVisibility) {
const columnVisibility = this.state.columnVisibility;
const index = this.findWithAttr(columnVisibility, "column", props.column);
columnVisibility[index].visible = !columnVisibility[index].visible;
this.forceUpdate();
};
findWithAttr(array: any, attr: string, value: string) {
for (let i = 0; i < array.length; i += 1) {
if (array[i][attr] === value) {
return i;
}
}
return -1;
};
};

View file

@ -1,92 +0,0 @@
import * as React from "react";
import { Home } from "./Home";
import { ILicense } from "./interfaces/ILicense";
import { IList } from "./interfaces/IList";
import { IMaintainer } from "./interfaces/IMaintainer";
import { ISoftware } from "./interfaces/ISoftware";
import { ISyntax } from "./interfaces/ISyntax";
import { ITag } from "./interfaces/ITag";
import { Language } from "./interfaces/Language";
interface IState {
languages: Language[];
licenses: ILicense[];
lists: IList[];
maintainers: IMaintainer[];
ruleCount: number;
software: ISoftware[];
syntaxes: ISyntax[];
tags: ITag[];
};
export class HomeContainer extends React.Component<{}, IState> {
constructor(props: any) {
super(props);
this.state = {
languages: [],
licenses: [],
lists: [],
maintainers: [],
ruleCount: 0,
software: [],
syntaxes: [],
tags: []
};
}
componentDidMount() {
this.fetchLists();
this.fetchLanguages();
this.fetchLicenses();
this.fetchMaintainers();
this.fetchSoftware();
this.fetchSyntaxes();
this.fetchTags();
};
fetchLanguages() {
fetch("/api/v1/languages")
.then(response => response.json())
.then(json => { this.setState({ languages: json }); })
};
fetchLicenses() {
fetch("/api/v1/licenses")
.then(response => response.json())
.then(json => { this.setState({ licenses: json }); })
};
fetchLists() {
fetch("/api/v1/lists")
.then(response => response.json())
.then(json => { this.setState({ lists: json }); })
};
fetchMaintainers() {
fetch("/api/v1/maintainers")
.then(response => response.json())
.then(json => { this.setState({ maintainers: json }); })
};
fetchSoftware() {
fetch("/api/v1/software")
.then(response => response.json())
.then(json => { this.setState({ software: json }); })
};
fetchSyntaxes() {
fetch("/api/v1/syntaxes")
.then(response => response.json())
.then(json => { this.setState({ syntaxes: json }); })
};
fetchTags() {
fetch("/api/v1/tags")
.then(response => response.json())
.then(json => { this.setState({ tags: json }); })
};
render() {
return <Home {...this.state} />;
};
};

View file

@ -1,28 +0,0 @@
import { ILicense } from "../interfaces/ILicense";
import { IMaintainer } from "../interfaces/IMaintainer";
import { ISyntax } from "../interfaces/ISyntax";
import { ITag } from "../interfaces/ITag";
import { Language } from "../interfaces/Language";
export interface IListDetails {
id: number;
chatUrl: string;
description: string;
descriptionSourceUrl: string;
donateUrl: string;
emailAddress: string;
forumUrl: string;
homeUrl: string;
issuesUrl: string;
languages: Language[];
license: ILicense;
maintainers: IMaintainer[];
name: string;
policyUrl: string;
publishedDate: string;
submissionUrl: string;
syntax: ISyntax;
tags: ITag[];
viewUrl: string;
viewUrlMirrors: string[];
};

View file

@ -1,14 +0,0 @@
import * as React from "react";
interface Props {
listCount: number;
};
export const Oneliner = (props: Props) =>
props.listCount > 0
? <p className="ml-2 mr-2">
The independent, comprehensive directory of <strong>{props.listCount.toLocaleString()}</strong> filter and host lists for advertisements, trackers, malware, and annoyances.
</p>
: <p className="ml-2 mr-2">
The independent, comprehensive directory of filter and host lists for advertisements, trackers, malware, and annoyances.
</p>;

View file

@ -1,67 +0,0 @@
import * as React from "react";
import { getContrast } from "../../../utils";
import { ITag } from "../interfaces/ITag";
interface Props {
tags: ITag[];
};
export const TagGroup = (props: Props) =>
props.tags && props.tags.length > 0
? <div className="fl-wrap-cell">
{props.tags.map((t: ITag, i: number) => <Tag tag={t} key={i} />)}
</div>
: null;
interface ITagProps {
tag: ITag;
};
const Tag = (props: ITagProps) => {
if (props.tag) {
const hexColor = kelly_colors_hex[props.tag.id % kelly_colors_hex.length];
return <span className="badge"
style={{
backgroundColor: `#${hexColor}`,
color: getContrast(hexColor)
}}
title={props.tag.description}>
{props.tag.name}
</span>;
} else {
return null;
}
};
//https://stackoverflow.com/a/4382138/2343739
const kelly_colors_hex = [
"FFB300", // Vivid Yellow
"803E75", // Strong Purple
"FF6800", // Vivid Orange
"A6BDD7", // Very Light Blue
"C10020", // Vivid Red
"CEA262", // Grayish Yellow
"817066", // Medium Gray
"007D34", // Vivid Green
"F6768E", // Strong Purplish Pink
"00538A", // Strong Blue
"FF7A5C", // Strong Yellowish Pink
"53377A", // Strong Violet
"FF8E00", // Vivid Orange Yellow
"B32851", // Strong Purplish Red
"F4C800", // Vivid Greenish Yellow
"7F180D", // Strong Reddish Brown
"93AA00", // Vivid Yellowish Green
"593315", // Deep Yellowish Brown
"F13A13", // Vivid Reddish Orange
"232C16", // Dark Olive Green
"000000", // Black
"FFFFFF", // White
"C2F4BE", // Light Green
"1EDBB9", // Medium Greenish Blue
"890000", // Dark Red
"474747", // Dark Grey
"BFA125", // Matte Gold
"A7823C", // Dark Beige
"035A5C" // Dark Greenish Blue
];

View file

@ -1,28 +0,0 @@
import * as React from "react";
import { IColumnVisibility } from "../../interfaces/IColumnVisibility";
import { ISoftware } from "../../interfaces/ISoftware";
import { IListDetails } from "../IListDetails";
import { InfoCard } from "./infoCard";
import { LinkButtonGroup } from "./LinkButtonGroup";
import { MaintainersInfoCard } from "./maintainersInfoCard";
interface Props {
columnVisibility: IColumnVisibility[];
list: IListDetails;
software: ISoftware[];
};
export const DetailsExpander = (props: Props) =>
<div className="card border-primary">
<div className="card-body p-2">
<div className="container m-0">
<div className="d-flex flex-row justify-content-between">
<InfoCard columnVisibility={props.columnVisibility} software={props.software} {...props.list} />
<LinkButtonGroup {...props.list} />
</div>
<div className="row">
<MaintainersInfoCard maintainers={props.list.maintainers} />
</div>
</div>
</div>
</div>;

View file

@ -1,31 +0,0 @@
import * as React from "react";
import { ChatButton, DonateButton, EmailButton, ForumButton, HomeButton, IssuesButton, PolicyButton, SubmitButton, SubscribeButton, ViewButton } from "../linkButtons";
import "./linkButtonGroup.css";
interface Props {
chatUrl: string;
donateUrl: string;
emailAddress: string;
forumUrl: string;
homeUrl: string;
issuesUrl: string;
name: string;
policyUrl: string;
submissionUrl: string;
viewUrl: string;
viewUrlMirrors: string[];
};
export const LinkButtonGroup = (props: Props) =>
<div className="btn-group-vertical">
<SubscribeButton name={props.name} url={props.viewUrl} urlMirrors={props.viewUrlMirrors} />
<ViewButton name={props.name} url={props.viewUrl} urlMirrors={props.viewUrlMirrors} />
<HomeButton name={props.name} url={props.homeUrl} />
<PolicyButton name={props.name} url={props.policyUrl} />
<DonateButton name={props.name} url={props.donateUrl} />
<IssuesButton name={props.name} url={props.issuesUrl} />
<ForumButton name={props.name} url={props.forumUrl} />
<ChatButton name={props.name} url={props.chatUrl} />
<SubmitButton name={props.name} url={props.submissionUrl} />
<EmailButton name={props.name} emailAddress={props.emailAddress} />
</div>;

View file

@ -1,3 +0,0 @@
import { DetailsExpander } from "./DetailsExpander";
export { DetailsExpander };

View file

@ -1,16 +0,0 @@
import * as React from "react";
import "./description.css";
interface Props {
description: string;
url: string;
};
export const Description = (props: Props) =>
props.description
? <h3 className="card-header fl-description">
{props.url
? <blockquote cite={props.url} className="m-0">{props.description}</blockquote>
: props.description}
</h3>
: null;

View file

@ -1,51 +0,0 @@
import * as React from "react";
import { IColumnVisibility } from "../../../interfaces/IColumnVisibility";
import { ILicense } from "../../../interfaces/ILicense";
import { ISoftware } from "../../../interfaces/ISoftware";
import { ISyntax } from "../../../interfaces/ISyntax";
import { ITag } from "../../../interfaces/ITag";
import { Language } from "../../../interfaces/Language";
import { SoftwareIcon } from "../../softwareIcon";
import { TagGroup } from "../../TagGroup";
import { Description } from "./Description";
import { Languages } from "./Languages";
import { License } from "./License";
import { PublishedDate } from "./PublishedDate";
import { Syntax } from "./Syntax";
interface Props {
columnVisibility: IColumnVisibility[];
description: string;
descriptionSourceUrl: string;
languages: Language[];
license: ILicense;
name: string;
publishedDate: string;
software: ISoftware[];
syntax: ISyntax;
tags: ITag[];
};
export const InfoCard = (props: Props) =>
<div className="flex-fill">
{props.columnVisibility.filter((c: IColumnVisibility) => c.column === "Tags")[0].visible
? null
: <TagGroup tags={props.tags} />}
<div>
{props.columnVisibility.filter((c: IColumnVisibility) => c.column === "Software")[0].visible
? null
: props.syntax
? props.software.filter((s: ISoftware) => s.syntaxIds.indexOf(props.syntax.id) > -1)
.map((s: ISoftware, i: number) => <SoftwareIcon id={s.id} key={i} />)
: null}
</div>
<Description {...props} url={props.descriptionSourceUrl} />
<ul className="list-group list-group-flush">
<Languages {...props} />
<PublishedDate date={props.publishedDate} />
<Syntax {...props} />
{props.columnVisibility.filter((c: IColumnVisibility) => c.column === "License")[0].visible
? null
: <License {...props} />}
</ul>
</div>;

Some files were not shown because too many files have changed in this diff Show more