Merge pull request #936 from collinbarrett/antd2
New UI build on AntDesign
4
src/FilterLists.Web/.gitignore
vendored
|
|
@ -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*
|
||||
9
src/FilterLists.Web/config-overrides.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { override, fixBabelImports } = require('customize-cra');
|
||||
|
||||
module.exports = override(
|
||||
fixBabelImports('import', {
|
||||
libraryName: 'antd',
|
||||
libraryDirectory: 'es',
|
||||
style: 'css',
|
||||
}),
|
||||
);
|
||||
BIN
src/FilterLists.Web/package-lock.json
generated
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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()}
|
||||
<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>;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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} />
|
||||
11
src/FilterLists.Web/src/components/Description.tsx
Normal 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>;
|
||||
31
src/FilterLists.Web/src/components/LicenseTag.tsx
Normal 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}
|
||||
</>;
|
||||
26
src/FilterLists.Web/src/components/LinkButton.tsx
Normal 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;
|
||||
22
src/FilterLists.Web/src/components/ListInfoButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
122
src/FilterLists.Web/src/components/ListInfoDrawer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
13
src/FilterLists.Web/src/components/PublishedDate.tsx
Normal 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;
|
||||
95
src/FilterLists.Web/src/components/SubscribeButtons.tsx
Normal 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}&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 };
|
||||
};
|
||||
31
src/FilterLists.Web/src/components/SyntaxTag.tsx
Normal 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}
|
||||
</>;
|
||||
3
src/FilterLists.Web/src/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { ListsTable } from './listsTable';
|
||||
|
||||
export { ListsTable };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */
|
||||
.grow {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { LanguageCloud } from './LanguageCloud';
|
||||
|
||||
export { LanguageCloud };
|
||||
36
src/FilterLists.Web/src/components/listsTable/ListDrawer.tsx
Normal 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} />;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
145
src/FilterLists.Web/src/components/listsTable/ListsTable.tsx
Normal 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} />
|
||||
{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>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
18
src/FilterLists.Web/src/components/listsTable/arraySorter.ts
Normal 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;
|
||||
3
src/FilterLists.Web/src/components/listsTable/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { ListsTableHoc } from './ListsTableHoc';
|
||||
|
||||
export { ListsTableHoc as ListsTable };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/* allow for 2nd row's bottom border to not be chopped in AllListsTable */
|
||||
.grow {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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" }
|
||||
};
|
||||
|
Before Width: | Height: | Size: 623 B After Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 921 B After Width: | Height: | Size: 921 B |
|
Before Width: | Height: | Size: 851 B After Width: | Height: | Size: 851 B |
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 658 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 547 B After Width: | Height: | Size: 547 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 636 B |
|
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 789 B After Width: | Height: | Size: 789 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 750 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 713 B |
|
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 867 B |
|
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 583 B After Width: | Height: | Size: 583 B |
|
Before Width: | Height: | Size: 693 B After Width: | Height: | Size: 693 B |
|
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 633 B |
|
|
@ -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
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { SoftwareCloud } from './SoftwareCloud';
|
||||
import { SoftwareIcon } from './SoftwareIcon';
|
||||
|
||||
export { SoftwareCloud, SoftwareIcon };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */
|
||||
.grow {
|
||||
max-height: none !important;
|
||||
}
|
||||
19
src/FilterLists.Web/src/components/tagCloud/TagCloud.tsx
Normal 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;
|
||||
3
src/FilterLists.Web/src/components/tagCloud/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { TagCloud } from './TagCloud';
|
||||
|
||||
export { TagCloud };
|
||||
19
src/FilterLists.Web/src/hooks/index.ts
Normal 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
|
||||
};
|
||||
10
src/FilterLists.Web/src/hooks/useApiData.tsx
Normal 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;
|
||||
};
|
||||
5
src/FilterLists.Web/src/hooks/useLanguages.tsx
Normal 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));
|
||||
5
src/FilterLists.Web/src/hooks/useLicenses.tsx
Normal 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));
|
||||
11
src/FilterLists.Web/src/hooks/useLists.tsx
Normal 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));
|
||||
};
|
||||
57
src/FilterLists.Web/src/hooks/useSearchColumnFilter.tsx
Normal 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;
|
||||
};
|
||||
5
src/FilterLists.Web/src/hooks/useSoftware.tsx
Normal 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));
|
||||
5
src/FilterLists.Web/src/hooks/useSyntaxes.tsx
Normal 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));
|
||||
21
src/FilterLists.Web/src/hooks/useTablePageSizer.tsx
Normal 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
|
||||
});
|
||||
5
src/FilterLists.Web/src/hooks/useTags.tsx
Normal 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));
|
||||
|
|
@ -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'));
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export interface Language {
|
||||
export interface Language {
|
||||
id: number;
|
||||
filterListIds: number[];
|
||||
iso6391: string;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export interface ILicense {
|
||||
export interface License {
|
||||
id: number;
|
||||
descriptionUrl: string;
|
||||
filterListIds: number[];
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export interface ISoftware {
|
||||
export interface Software {
|
||||
id: number;
|
||||
homeUrl: string;
|
||||
isAbpSubscribable: boolean;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export interface ISyntax {
|
||||
export interface Syntax {
|
||||
id: number;
|
||||
definitionUrl: string;
|
||||
filterListIds: number[];
|
||||
6
src/FilterLists.Web/src/interfaces/Tag.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface Tag {
|
||||
id: number;
|
||||
description: string;
|
||||
filterListIds: number[];
|
||||
name: string;
|
||||
};
|
||||
|
|
@ -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: {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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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} />;
|
||||
};
|
||||
};
|
||||
|
|
@ -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[];
|
||||
};
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { DetailsExpander } from "./DetailsExpander";
|
||||
|
||||
export { DetailsExpander };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||