refactor GUI (primarily detailsExpander)

This commit is contained in:
Collin Barrett 2018-09-14 20:36:08 -05:00
parent a59b2a4d51
commit 864286694e
54 changed files with 962 additions and 542 deletions

View file

@ -13,6 +13,7 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_CHAINED_METHOD_CALLS/@EntryValue">CHOP_IF_LONG</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/ALIGN_TERNARY/@EntryValue">ALIGN_ALL</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpFileLayoutPatterns/Pattern/@EntryValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&#xD;
&lt;Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"&gt;&#xD;
&lt;TypePattern DisplayName="Non-reorderable types"&gt;&#xD;

View file

@ -2,10 +2,8 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";
import { BrowserRouter } from "react-router-dom";
import * as RoutesModule from "./routes";
import "bootstrap";
import "./css/site.css";
let routes = RoutesModule.routes;
import * as RoutesModule from "./Routes";
let routes = RoutesModule.Routes;
function renderApp() {
const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")!;
@ -22,7 +20,7 @@ renderApp();
if (module.hot) {
module.hot.accept("./routes",
() => {
routes = require<typeof RoutesModule>("./routes").routes;
routes = require<typeof RoutesModule>("./Routes").Routes;
renderApp();
});
}

View file

@ -0,0 +1,63 @@
import * as React from "react";
import "bootstrap";
import "./site.css";
interface ILayoutProps {
children?: React.ReactNode;
}
export const Layout = (props: ILayoutProps) => {
return <div className="container">
<Header/>
<div className="row">
<div className="w-100">
{ props.children }
</div>
</div>
<Footer/>
</div>;
};
const Header = () => {
return <header className="row">
<h1>
<a href="./">
<img src="logo_filterlists.png" alt="FilterLists" className="img-fluid"/>
</a>
</h1>
</header>;
};
const Footer = () => {
return <footer className="row justify-content-center">
<p className="mt-2 ml-1 mr-1">
<ApiLink/> | <GitHubLink/> | <DonateLink/> | By <OwnerLink/>
</p>
</footer>;
};
const ApiLink = () => {
return <a href="https://filterlists.com/api/docs/index.html"
title="API Swagger Docs">
API
</a>;
};
const GitHubLink = () => {
return <a href="https://github.com/collinbarrett/FilterLists">
GitHub
</a>;
};
const DonateLink = () => {
return <a href="https://beerpay.io/collinbarrett/FilterLists"
title="Support with Beerpay">
Donate
</a>;
};
const OwnerLink = () => {
return <a href="https://collinmbarrett.com/">
Collin M. Barrett
</a>;
};

View file

@ -0,0 +1,8 @@
import * as React from "react";
import { Layout } from "./Layout";
import { Route } from "react-router-dom";
import { Home } from "./modules";
export const Routes = <Layout>
<Route exact path="/" component={ Home }/>
</Layout>;

View file

@ -1,29 +0,0 @@
import * as React from "react";
export interface ILayoutProps {
children?: React.ReactNode;
}
export class Layout extends React.Component<ILayoutProps, {}> {
render() {
return <div className="container">
<div className="row">
<h1>
<a href="./">
<img src="logo_filterlists.png" alt="FilterLists" className="img-fluid"/>
</a>
</h1>
</div>
<div className="row">
<div className="w-100">
{ this.props.children }
</div>
</div>
<div className="row justify-content-center">
<p className="mt-2 ml-1 mr-1">
<a href="https://filterlists.com/api/docs/index.html">API</a> | <a href="https://github.com/collinbarrett/FilterLists">GitHub</a> | <a href="https://beerpay.io/collinbarrett/FilterLists">Donate</a> | By <a href="https://collinmbarrett.com/">Collin M. Barrett</a>
</p>
</div>
</div>;
};
}

View file

@ -1,483 +0,0 @@
import * as React from "react";
import "isomorphic-fetch";
import * as moment from "moment";
export default class ListDetails extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
responseLoaded: false,
listId: props.listId
};
this.fetchData();
}
fetchData() {
fetch(`https://filterlists.com/api/v1/lists/${this.state.listId}`)
.then(response => response.json() as Promise<IFilterListDetailsDto[]>)
.then(data => {
this.setState({
filterListDetails: data,
responseLoaded: true
});
});
}
render() {
return this.state.responseLoaded
? <div>
<FilterListDetails details={this.state.filterListDetails}/>
</div>
: <div>Loading...</div>;
}
//https://stackoverflow.com/a/11868398/2343739
static getContrast(hexcolor: string) {
const r = parseInt(hexcolor.substr(0, 2), 16);
const g = parseInt(hexcolor.substr(2, 2), 16);
const b = parseInt(hexcolor.substr(4, 2), 16);
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? "black" : "white";
}
}
function FilterListDetails(props: any) {
return <div className="card border-primary">
<div className="card-body p-2">
<div className="container m-0">
<div className="row">
<ListInfo details={props.details}/>
<ListUrls details={props.details}/>
</div>
<div className="row">
<Maintainers maintainers={props.details.maintainers}/>
</div>
</div>
</div>
</div>;
}
function ListInfo(props: any) {
return <div className="col-9">
<Tags tags={props.details.tags}/>
<Description description={props.details.description} url={props.details.descriptionSourceUrl}/>
<ul className="list-group list-group-flush">
<Languages languages={props.details.languages}/>
<RuleCount count={props.details.ruleCount}/>
<DiscontinuedDate date={props.details.discontinuedDate}/>
{props.details.discontinuedDate ? null : <UpdatedDate date={props.details.updatedDate}/>}
<PublishedDate date={props.details.publishedDate}/>
<License license={props.details.license}/>
</ul>
</div>;
}
function ListUrls(props: any) {
return <div className="col-3 p-0 btn-group-vertical justify-content-start d-flex align-items-end">
<SubscribeUrl url={props.details.viewUrl} name={props.details.name}/>
<SubscribeUrlMirror url={props.details.viewUrlMirror1} name={props.details.name}/>
<SubscribeUrlMirror url={props.details.viewUrlMirror2} name={props.details.name}/>
<ViewUrl url={props.details.viewUrl} name={props.details.name}/>
<ViewUrlMirror url={props.details.viewUrlMirror1} name={props.details.name}/>
<ViewUrlMirror url={props.details.viewUrlMirror2} name={props.details.name}/>
<HomeUrl url={props.details.homeUrl} name={props.details.name}/>
<PolicyUrl url={props.details.policyUrl} name={props.details.name}/>
<DonateUrl url={props.details.donateUrl} name={props.details.name}/>
<IssuesUrl url={props.details.issuesUrl} name={props.details.name}/>
<ForumUrl url={props.details.forumUrl} name={props.details.name}/>
<ChatUrl url={props.details.chatUrl} name={props.details.name}/>
<SubmissionUrl url={props.details.submissionUrl} name={props.details.name}/>
<EmailAddress email={props.details.emailAddress} name={props.details.name}/>
</div>;
}
function Tags(props: any) {
return props.tags.length > 0
? <div className="d-md-none">{props.tags.map(
(tag: any) => <span className="badge" style={{
backgroundColor: `#${tag.colorHex}`,
color: ListDetails.getContrast(`${tag.colorHex}`)
}} title={tag.description}>{tag.name}</span>)}</div>
: null;
}
function Description(props: any) {
return props.description
? (props.url
? <h3 className="card-header fl-description">
<blockquote cite={props.url} className="m-0">{props.description}</blockquote>
</h3>
: <h3 className="card-header fl-description">{props.description}</h3>)
: null;
}
function Languages(props: any) {
return props.languages.length > 0
? props.languages.length > 1
? <li className="list-group-item">
<p className="m-0">Languages:</p>
<ul>
{props.languages.map((language: any) => <li>{language}</li>)}
</ul>
</li>
: <li className="list-group-item">
<p>Language: {props.languages.map((language: any) => language)}</p>
</li>
: null;
}
function RuleCount(props: any) {
return props.count > 0
? <li className="list-group-item">
<p>Rule Count: {props.count.toLocaleString()}</p>
</li>
: null;
}
function DiscontinuedDate(props: any) {
return props.date
? <li className="list-group-item">
<p>Discontinued: {moment(props.date).format("l")}</p>
</li>
: null;
}
function UpdatedDate(props: any) {
return props.date
? <li className="d-md-none list-group-item">
<p>Updated: {moment(props.date).isValid() ? moment(props.date).format("l") : "N/A"}</p>
</li>
: null;
}
function PublishedDate(props: any) {
return props.date
? <li className="list-group-item">
<p>Published: {moment(props.date).format("l")}</p>
</li>
: null;
}
function License(props: any) {
return props.license
? (props.license.descriptionUrl
? <li className="list-group-item">
<p>License: <a href={props.license.descriptionUrl}>{props.license.name}</a></p>
</li>
: <li className="list-group-item">
<p>License: {props.license.name}</p>
</li>)
: null;
}
function SubscribeUrl(props: any) {
return props.url.indexOf("https://") === -1
? SubscribeUrlNotSecure()
: props.url.indexOf("web.archive.org") === -1
? SubscribeUrlPrimary()
: SubscribeUrlWayback();
function SubscribeUrlPrimary() {
return <a href={`abp:subscribe?location=${encodeURIComponent(props.url)}&amp;title=${encodeURIComponent(props.name)}`}
className="btn btn-primary btn-block fl-btn-details-action"
title={`Subscribe to list with browser extension supporting \"abp:\" protocol (e.g. uBlock Origin, AdBlock Plus).`}>
Subscribe
</a>;
}
function SubscribeUrlWayback() {
return <a href={`abp:subscribe?location=${encodeURIComponent(props.url)}&amp;title=${encodeURIComponent(props.name)}`}
className="btn btn-secondary btn-block fl-btn-details-action"
title={`Archive.org Mirror (Original Offline) - Subscribe to list with browser extension supporting \"abp:\" protocol (e.g. uBlock Origin, AdBlock Plus).`}>
Subscribe
</a>;
}
function SubscribeUrlNotSecure() {
return <a href={`abp:subscribe?location=${encodeURIComponent(props.url)}&amp;title=${encodeURIComponent(props.name)}`}
className="btn btn-danger btn-block fl-btn-details-action"
title={`Not Secure - Subscribe to list with browser extension supporting \"abp:\" protocol (e.g. uBlock Origin, AdBlock Plus).`}>
Subscribe
</a>;
}
};
function SubscribeUrlMirror(props: any) {
return props.url
? props.url.indexOf("https://") === -1
? SubscribeUrlNotSecure()
: SubscribeUrlSecondary()
: null;
function SubscribeUrlSecondary() {
return <a href={`abp:subscribe?location=${encodeURIComponent(props.url)}&amp;title=${encodeURIComponent(props.name)}`}
className="btn btn-secondary btn-block fl-btn-details-action"
title={`Mirror - Subscribe to list with browser extension supporting \"abp:\" protocol (e.g. uBlock Origin, AdBlock Plus).`}>
Subscribe
</a>;
}
function SubscribeUrlNotSecure() {
return <a href={`abp:subscribe?location=${encodeURIComponent(props.url)}&amp;title=${encodeURIComponent(props.name)}`}
className="btn btn-danger btn-block fl-btn-details-action"
title={`Mirrow - Not Secure - Subscribe to list with browser extension supporting \"abp:\" protocol (e.g. uBlock Origin, AdBlock Plus).`}>
Subscribe
</a>;
}
};
function ViewUrl(props: any) {
return props.url.indexOf("https://") === -1
? ViewUrlNotSecure()
: props.url.indexOf("web.archive.org") === -1
? ViewUrlPrimary()
: ViewUrlWayback();
function ViewUrlPrimary() {
return <a href={props.url}
className="btn btn-primary fl-btn-details-action"
title={`View ${props.name} in its raw format.`}>
View
</a>;
}
function ViewUrlWayback() {
return <a href={props.url}
className="btn btn-secondary fl-btn-details-action"
title={`Archive.org Mirror (Original Offline) - View ${props.name} in its raw format.`}>
View
</a>;
}
function ViewUrlNotSecure() {
return <a href={props.url}
className="btn btn-danger fl-btn-details-action"
title={`Not Secure - View ${props.name} in its raw format.`}>
View
</a>;
}
};
function ViewUrlMirror(props: any) {
return props.url
? props.url.indexOf("https://") === -1
? ViewUrlNotSecure()
: viewUrlSecondary()
: null;
function viewUrlSecondary() {
return <a href={props.url}
className="btn btn-secondary fl-btn-details-action"
title={`Mirror - View ${props.name} in its raw format.`}>
View
</a>;
}
function ViewUrlNotSecure() {
return <a href={props.url}
className="btn btn-danger fl-btn-details-action"
title={`Mirror - Not Secure - View ${props.name} in its raw format.`}>
View
</a>;
}
};
function HomeUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`View the home page for ${props.name}.`}>
Home
</a>
: null;
}
function PolicyUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`View the policy for which rules ${props.name} includes.`}>
Policy
</a>
: null;
}
function DonateUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`Donate to support ${props.name}.`}>
Donate
</a>
: null;
}
function IssuesUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`View the GitHub Issues for ${props.name}.`}>
GH Issues
</a>
: null;
}
function ForumUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`View the forum for ${props.name}.`}>
Forum
</a>
: null;
}
function ChatUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`Enter the chat room for ${props.name}.`}>
Chat
</a>
: null;
}
function SubmissionUrl(props: any) {
return props.url
? <a href={props.url} className="btn btn-primary fl-btn-details-action"
title={`Submit a new rule to be included in ${props.name}.`}>
Add Rule
</a>
: null;
}
function EmailAddress(props: any) {
return props.email
? <a href={`mailto:${props.email}`} className="btn btn-primary fl-btn-details-action"
title={`Email ${props.name}.`}>
Email
</a>
: null;
}
function Maintainers(props: any) {
return props.maintainers.length > 0
? <div className="w-100">
{props.maintainers.map(
(maintainer: any) => <Maintainer maintainer={maintainer} key={maintainer.id.toString()}/>)}
</div>
: null;
}
function Maintainer(props: any) {
return <div className="card">
<div className="card-body">
<h3 className="card-header">Maintained by {props.maintainer.name}</h3>
<div className="container pt-1">
<div className="row">
<MaintainerAdditionalLists maintainer={props.maintainer}/>
<MaintainerUrls maintainer={props.maintainer}/>
</div>
</div>
</div>
</div>;
}
function MaintainerAdditionalLists(props: any) {
return <div className="col-9">
{props.maintainer.additionalLists.length > 0
? <div>
<h4>More by {props.maintainer.name}:</h4>
<ul>
{props.maintainer.additionalLists.map(
(list: any) => <MaintainerAdditionalList list={list} key={list.id.toString()}/>)}
</ul>
</div>
: null}
</div>;
}
function MaintainerAdditionalList(props: any) {
return <li>{props.list.name}</li>;
}
function MaintainerUrls(props: any) {
return <div className="col-3 p-0 btn-group-vertical justify-content-start d-flex align-items-end" role="group">
{props.maintainer.homeUrl
? <a href={props.maintainer.homeUrl} className="btn btn-primary fl-btn-details-action"
title={`View the home page of ${props.maintainer.name}.`}>
Home
</a>
: null}
{props.maintainer.emailAddress
? <a href={`mailto:${props.maintainer.emailAddress}`}
className="btn btn-primary fl-btn-details-action"
title={`Email ${props.maintainer.name}.`}>
Email
</a>
: null}
{props.maintainer.twitterHandle
? <a href={`https://twitter.com/${props.maintainer.twitterHandle}`}
className="btn btn-primary fl-btn-details-action"
title={`View the Twitter page of ${props.maintainer.name}.`}>
Twitter
</a>
: null}
</div>;
}
interface IFilterListDetailsDto {
chatUrl: string;
description: string;
descriptionSourceUrl: string;
discontinuedDate: string;
donateUrl: string;
emailAddress: string;
forumUrl: string;
homeUrl: string;
issuesUrl: string;
languages: string[];
license: IListLicenseDto;
maintainers: IListMaintainerDto[];
policyUrl: string;
publishedDate: string;
ruleCount: number;
submissionUrl: string;
syntax: IListSyntaxDto[];
tags: IListTagDto[];
updatedDate: string;
viewUrl: string;
viewUrlMirror1: string;
viewUrlMirror2: string;
}
interface IListLicenseDto {
descriptionUrl: string;
name: string;
}
interface IListMaintainerDto {
id: number;
emailAddress: string;
homeUrl: string;
name: string;
twitterHandle: string;
additionalLists: IMaintainerAdditionalListsDto[];
}
interface IMaintainerAdditionalListsDto {
id: number;
name: string;
}
interface IListSyntaxDto {
definitionUrl: string;
name: string;
supportedSoftware: ISyntaxSupportedSoftwareDto[];
}
interface IListTagDto {
name: string;
colorHex: string;
description: string;
}
interface ISyntaxSupportedSoftwareDto {
homeUrl: string;
name: string;
};

View file

@ -1,10 +1,13 @@
import * as React from "react";
import { RouteComponentProps } from "react-router";
import "isomorphic-fetch";
import "../../utils/loader.css";
import ReactTable from "react-table"
import "react-table/react-table.css"
import ListDetails from "./ListDetails";
import "./home.css";
import * as moment from "moment";
import { getContrast } from "../../utils/GetContrast";
import { DetailsExpanderContainer } from "./components";
interface IHomeState {
lists: IListDto[];
@ -49,9 +52,7 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
this.state.loadingRuleCount ||
this.state.loadingSoftware ||
this.state.loadingLanguages
? <p>
<em>Loading...</em>
</p>
? <div className="loader">Loading...</div>
: <div>
{Home.renderTagline(this.state)}
{Home.renderFilterListsTable(this.state)}
@ -166,7 +167,7 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
Cell: (cell: any) => <div className="fl-tag-container">{cell.value.map(
(e: any) => <span className="badge" style={{
backgroundColor: `#${e.colorHex}`,
color: ListDetails.getContrast(`${e.colorHex}`)
color: getContrast(`${e.colorHex}`)
}} title={e.description}>{e.name}</span>)}</div>,
width: 200,
headerClassName: "d-none d-md-block",
@ -247,7 +248,7 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
]}
SubComponent={(row: any) => {
return (
<ListDetails listId={row.original.id}/>
<DetailsExpanderContainer listId={row.original.id}/>
);
}}
className="-striped -highlight"/>;
@ -255,14 +256,17 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
private renderColumnVisibilityCheckboxes() {
return <div className="d-none d-md-block text-right">
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map((c: IColumnVisibility) => this.renderColumnVisibilityCheckbox(c))}
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map(
(c: IColumnVisibility) => this.renderColumnVisibilityCheckbox(c))}
</div>;
};
private renderColumnVisibilityCheckbox(props: IColumnVisibility) {
return <div className="form-check form-check-inline">
<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>
<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>;
}

View file

@ -0,0 +1,21 @@
import * as React from "react";
import { IFilterListDetailsDto } from "./IFilterListDetailsDto";
import { InfoCard } from "./infoCard";
import { LinkButtonGroup } from "./LinkButtonGroup";
import { MaintainersInfoCard } from "./maintainersInfoCard";
export const DetailsExpander = (props: IFilterListDetailsDto) => {
return <div className="card border-primary">
<div className="card-body p-2">
<div className="container m-0">
<div className="row">
<InfoCard {...props}/>
<LinkButtonGroup {...props}/>
</div>
<div className="row">
<MaintainersInfoCard maintainers={props.maintainers}/>
</div>
</div>
</div>
</div>;
};

View file

@ -0,0 +1,42 @@
import * as React from "react";
import "isomorphic-fetch";
import "../../../../utils/loader.css";
import { IFilterListDetailsDto } from "./IFilterListDetailsDto";
import { DetailsExpander } from "./DetailsExpander";
interface IProps {
listId: number;
}
interface IState {
filterListDetails: IFilterListDetailsDto;
}
export class DetailsExpanderContainer extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
filterListDetails: {} as IFilterListDetailsDto
};
}
componentDidMount() {
this.fetchFilterListDetails();
}
fetchFilterListDetails() {
fetch(`https://filterlists.com/api/v1/lists/${this.props.listId}`)
.then(r => r.json() as Promise<IFilterListDetailsDto>)
.then(d => {
this.setState({
filterListDetails: d
});
});
}
render() {
return this.state.filterListDetails.name
? <DetailsExpander {...this.state.filterListDetails}/>
: <div className="loader">Loading...</div>;;
}
}

View file

@ -0,0 +1,61 @@
export interface IFilterListDetailsDto {
chatUrl: string;
description: string;
descriptionSourceUrl: string;
discontinuedDate: string;
donateUrl: string;
emailAddress: string;
forumUrl: string;
homeUrl: string;
issuesUrl: string;
languages: string[];
license: IListLicenseDto;
maintainers: IListMaintainerDto[];
name: string;
policyUrl: string;
publishedDate: string;
ruleCount: number;
submissionUrl: string;
syntax: IListSyntaxDto[];
tags: IListTagDto[];
updatedDate: string;
viewUrl: string;
viewUrlMirror1: string;
viewUrlMirror2: string;
}
export interface IListLicenseDto {
descriptionUrl: string;
name: string;
}
export interface IListMaintainerDto {
id: number;
emailAddress: string;
homeUrl: string;
name: string;
twitterHandle: string;
additionalLists: IMaintainerAdditionalListDto[];
}
export interface IListSyntaxDto {
definitionUrl: string;
name: string;
supportedSoftware: ISyntaxSupportedSoftwareDto[];
}
export interface IListTagDto {
name: string;
colorHex: string;
description: string;
}
export interface IMaintainerAdditionalListDto {
id: number;
name: string;
}
export interface ISyntaxSupportedSoftwareDto {
homeUrl: string;
name: string;
}

View file

@ -0,0 +1,44 @@
import * as React from "react";
import {
ChatButton,
DonateButton,
EmailButton,
ForumButton,
HomeButton,
IssuesButton,
PolicyButton,
SubmitButton,
SubscribeButtonGroup,
ViewButtonGroup
} from "../linkButtons";
interface IProps {
chatUrl: string;
donateUrl: string;
emailAddress: string;
forumUrl: string;
homeUrl: string;
issuesUrl: string;
name: string;
policyUrl: string;
submissionUrl: string;
viewUrl: string;
viewUrlMirror1: string;
viewUrlMirror2: string;
}
export const LinkButtonGroup = (props: IProps) => {
const viewUrlMirrors = new Array(props.viewUrlMirror1, props.viewUrlMirror2).filter(u => u);
return <div className="col-3 p-0 btn-group-vertical justify-content-start d-flex align-items-end">
<SubscribeButtonGroup {...props} url={props.viewUrl} urlMirrors={viewUrlMirrors}/>
<ViewButtonGroup {...props} url={props.viewUrl} urlMirrors={viewUrlMirrors}/>
<HomeButton {...props} url={props.homeUrl}/>
<PolicyButton {...props} url={props.policyUrl}/>
<DonateButton {...props} url={props.donateUrl}/>
<IssuesButton {...props} url={props.issuesUrl}/>
<ForumButton {...props} url={props.forumUrl}/>
<ChatButton {...props} url={props.chatUrl}/>
<SubmitButton {...props} url={props.submissionUrl}/>
<EmailButton {...props} emailAddress={props.emailAddress}/>
</div>;
};

View file

@ -0,0 +1,5 @@
import { DetailsExpanderContainer } from "./DetailsExpanderContainer";
export {
DetailsExpanderContainer
};

View file

@ -0,0 +1,17 @@
import * as React from "react";
import "./description.css";
interface IProps {
description: string;
url: string;
}
export const Description = (props: IProps) => {
return 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

@ -0,0 +1,14 @@
import * as React from "react";
import * as moment from "moment";
interface IProps {
date: string;
}
export const DiscontinuedDate = (props: IProps) => {
return props.date
? <li className="list-group-item">
<p>Discontinued: {moment(props.date).format("l")}</p>
</li>
: null;
};

View file

@ -0,0 +1,39 @@
import * as React from "react";
import { IListLicenseDto, IListSyntaxDto, IListTagDto } from "../IFilterListDetailsDto";
import { Description } from "./Description";
import { DiscontinuedDate } from "./DiscontinuedDate";
import { Languages } from "./Languages";
import { License } from "./License";
import { PublishedDate } from "./PublishedDate";
import { RuleCount } from "./RuleCount";
import { Tags } from "./Tags";
import { UpdatedDate } from "./UpdatedDate";
interface IProps {
description: string;
descriptionSourceUrl: string;
discontinuedDate: string;
languages: string[];
license: IListLicenseDto;
name: string;
publishedDate: string;
ruleCount: number;
syntax: IListSyntaxDto[];
tags: IListTagDto[];
updatedDate: string;
}
export const InfoCard = (props: IProps) => {
return <div className="col-9">
<Tags tags={props.tags}/>
<Description {...props} url={props.descriptionSourceUrl}/>
<ul className="list-group list-group-flush">
<Languages languages={props.languages}/>
<RuleCount {...props}/>
<DiscontinuedDate date={props.discontinuedDate}/>
<UpdatedDate {...props}/>
<PublishedDate date={props.publishedDate}/>
<License {...props.license}/>
</ul>
</div>;
};

View file

@ -0,0 +1,20 @@
import * as React from "react";
interface IProps {
languages: string[];
}
export const Languages = (props: IProps) => {
return props.languages.length > 0
? props.languages.length > 1
? <li className="list-group-item">
<p className="m-0">Languages:</p>
<ul>
{props.languages.map((language: any) => <li>{language}</li>)}
</ul>
</li>
: <li className="list-group-item">
<p>Language: {props.languages.map((language: any) => language)}</p>
</li>
: null;
};

View file

@ -0,0 +1,14 @@
import * as React from "react";
import { IListLicenseDto } from "../IFilterListDetailsDto";
export const License = (props: IListLicenseDto) => {
return props.name
? (props.descriptionUrl
? <li className="list-group-item">
<p>License: <a href={props.descriptionUrl}>{props.name}</a></p>
</li>
: <li className="list-group-item">
<p>License: {props.name}</p>
</li>)
: null;
};

View file

@ -0,0 +1,14 @@
import * as React from "react";
import * as moment from "moment";
interface IProps {
date: string;
}
export const PublishedDate = (props: IProps) => {
return props.date
? <li className="list-group-item">
<p>Published: {moment(props.date).format("l")}</p>
</li>
: null;
};

View file

@ -0,0 +1,13 @@
import * as React from "react";
interface IProps {
ruleCount: number;
}
export const RuleCount = (props: IProps) => {
return props.ruleCount > 0
? <li className="list-group-item">
<p>Rule Count: {props.ruleCount.toLocaleString()}</p>
</li>
: null;
};

View file

@ -0,0 +1,17 @@
import * as React from "react";
import { IListTagDto } from "../IFilterListDetailsDto";
import { getContrast } from "../../../../../utils/GetContrast";
export const Tag = (props: IListTagDto) => {
const style = {
backgroundColor: props.colorHex ? `#${props.colorHex}` : undefined,
color: props.colorHex ? getContrast(`${props.colorHex}`) : undefined
};
return props.name
? <span className="badge"
style={style}
title={props.description}>
{props.name}
</span>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { IListTagDto } from "../IFilterListDetailsDto";
import { Tag } from "./Tag";
interface IProps {
tags: IListTagDto[];
}
export const Tags = (props: IProps) => {
return props.tags.length > 0
? <div className="d-md-none">
{props.tags.map((t: IListTagDto) => <Tag {...t}/>)}
</div>
: null;
};

View file

@ -0,0 +1,19 @@
import * as React from "react";
import * as moment from "moment";
interface IProps {
discontinuedDate: string;
updatedDate: string;
}
export const UpdatedDate = (props: IProps) => {
return props.discontinuedDate
? null
: props.updatedDate
? <li className="d-md-none list-group-item">
<p>Updated: {moment(props.updatedDate).isValid()
? moment(props.updatedDate).format("l")
: "N/A"}</p>
</li>
: null;
};

View file

@ -0,0 +1 @@
.fl-description { margin: 0 0 .5rem; }

View file

@ -0,0 +1,5 @@
import { InfoCard } from "./InfoCard";
export {
InfoCard
};

View file

@ -0,0 +1,20 @@
import * as React from "react";
import { IMaintainerAdditionalListDto } from "../IFilterListDetailsDto";
interface IProps {
name: string;
additionalLists: IMaintainerAdditionalListDto[];
}
export const MaintainerAdditionalLists = (props: IProps) => {
return props.additionalLists.length > 0
? <div className="col-9">
<div>
<h4>More by {props.name}:</h4>
<ul>
{props.additionalLists.map((l: IMaintainerAdditionalListDto) => <li>{l.name}</li>)}
</ul>
</div>
</div>
: null;
};

View file

@ -0,0 +1,20 @@
import * as React from "react";
import { IListMaintainerDto } from "../IFilterListDetailsDto";
import { MaintainerAdditionalLists } from "./MaintainerAdditionalLists";
import { MaintainerLinkButtonGroup } from "./MaintainerLinkButtonGroup";
export const MaintainerInfoCard = (props: IListMaintainerDto) => {
return props.name
? <div className="card">
<div className="card-body">
<h3 className="card-header">Maintained by {props.name}</h3>
<div className="container pt-1">
<div className="row">
<MaintainerAdditionalLists {...props}/>
<MaintainerLinkButtonGroup {...props}/>
</div>
</div>
</div>
</div>
: null;
};

View file

@ -0,0 +1,17 @@
import * as React from "react";
import { EmailButton, HomeButton, TwitterButton } from "../../linkButtons";
interface IProps {
emailAddress: string;
homeUrl: string;
name: string;
twitterHandle: string;
}
export const MaintainerLinkButtonGroup = (props: IProps) => {
return <div className="col-3 p-0 btn-group-vertical justify-content-start d-flex align-items-end" role="group">
<HomeButton {...props} url={props.homeUrl}/>
<EmailButton {...props}/>
<TwitterButton {...props}/>
</div>;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { IListMaintainerDto } from "../IFilterListDetailsDto";
import { MaintainerInfoCard } from "./MaintainerInfoCard";
interface IProps {
maintainers: IListMaintainerDto[];
}
export const MaintainersInfoCard = (props: IProps) => {
return props.maintainers.length > 0
? <div className="w-100">
{props.maintainers.map((m: IListMaintainerDto) => <MaintainerInfoCard {...m}/>)}
</div>
: null;
};

View file

@ -0,0 +1,5 @@
import { MaintainersInfoCard } from "./MaintainersInfoCard";
export {
MaintainersInfoCard
};

View file

@ -0,0 +1,5 @@
import { DetailsExpanderContainer } from "./detailsExpanderContainer";
export {
DetailsExpanderContainer
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const ChatButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`Enter the chat room for ${props.name}.`}
text="Chat"/>
: null;
};;

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const DonateButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`Donate to ${props.name}.`}
text="Donate"/>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
emailAddress: string;
}
export const EmailButton = (props: IProps) => {
return props.emailAddress
? <LinkButton href={`mailto:${props.emailAddress}`}
title={`Email ${props.name}.`}
text="Email"/>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const ForumButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`View the forum for ${props.name}.`}
text="Forum"/>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const HomeButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`View ${props.name}'s homepage.`}
text="Home"/>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const IssuesButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`View the GitHub Issues for ${props.name}.`}
text="GH Issues"/>
: null;
};

View file

@ -0,0 +1,19 @@
import * as React from "react";
import "./button.css";
interface IProps {
href: string;
title?: string;
buttonClass?: string;
text: string;
}
export const LinkButton = (props: IProps) => {
return props.href
? <a href={props.href}
title={props.title}
className={`btn ${props.buttonClass || "btn-primary"} fl-btn-link`}>
{props.text}
</a>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const PolicyButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`View the types of rules that ${props.name} includes.`}
text="Policy"/>
: null;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
}
export const SubmitButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`Submit a new rule to be included in ${props.name}.`}
text="Submit"/>
: null;
};

View file

@ -0,0 +1,34 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
text?: string;
}
export const SubscribeButton = (props: IProps) => {
let buttonClass: string | undefined;
let titlePrefix: string;
if (props.url.indexOf("https://") === -1) {
buttonClass = "btn-danger";
titlePrefix = "Not Secure - ";
} else {
buttonClass = undefined;
titlePrefix = "";
}
const hrefTitle = `&amp;title=${encodeURIComponent(props.name)}`;
const href = `abp:subscribe?location=${encodeURIComponent(props.url)}${hrefTitle}`;
const title =
`${titlePrefix}Subscribe to ${props.name
} with a browser extension supporting the \"abp:\" protocol (e.g. uBlock Origin, Adblock Plus).`;
return props.url
? <LinkButton href={href}
title={title}
buttonClass={buttonClass}
text={props.text || "Subscribe"}/>
: null;
};

View file

@ -0,0 +1,52 @@
import * as React from "react";
import { SubscribeButton } from "./SubscribeButton";
interface IProps {
name: string;
url: string;
urlMirrors?: string[];
}
export const SubscribeButtonGroup = (props: IProps) => {
return props.url
? (props.urlMirrors && props.urlMirrors.length > 0)
? <SubscribeButtonGroupDropdown {...props} urlMirrors={props.urlMirrors}/>
: <SubscribeButton {...props}/>
: null;
};
interface ISubscribeButtonGroupDropdownProps {
name: string;
url: string;
urlMirrors: string[];
}
const SubscribeButtonGroupDropdown = (props: ISubscribeButtonGroupDropdownProps) => {
let firstButtonText: string = "Original";
let mirrorIndex: number = 0;
if (props.url.indexOf("web.archive.org") !== -1) {
firstButtonText = "Mirror 1";
mirrorIndex++;
}
return <div className="btn-group-vertical fl-btn-link" role="group">
<BtnGroupDropSubscribe/>
<div className="dropdown-menu" aria-labelledby="btnGroupDropSubscribe">
<SubscribeButton {...props} text={firstButtonText}/>
{props.urlMirrors.map(
(m, i) => <SubscribeButton {...props} url={m} text={`Mirror ${i + 1 + mirrorIndex}`}/>)}
</div>
</div>;
};
const BtnGroupDropSubscribe = () => {
return <button id="btnGroupDropSubscribe"
type="button"
className="btn btn-primary dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
Subscribe
</button>;
};

View file

@ -0,0 +1,15 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
twitterHandle: string;
}
export const TwitterButton = (props: IProps) => {
return props.twitterHandle
? <LinkButton href={`https://twitter.com/${props.twitterHandle}`}
title={`View ${props.name}'s Twitter profile.`}
text="Twitter"/>
: null;
};

View file

@ -0,0 +1,16 @@
import * as React from "react";
import { LinkButton } from "./LinkButton";
interface IProps {
name: string;
url: string;
text?: string;
}
export const ViewButton = (props: IProps) => {
return props.url
? <LinkButton href={props.url}
title={`View ${props.name} in its raw format.`}
text={props.text || "View"}/>
: null;
};

View file

@ -0,0 +1,52 @@
import * as React from "react";
import { ViewButton } from "./ViewButton";
interface IProps {
name: string;
url: string;
urlMirrors?: string[];
}
export const ViewButtonGroup = (props: IProps) => {
return props.url
? (props.urlMirrors && props.urlMirrors.length > 0)
? <ViewButtonGroupDropdown {...props} urlMirrors={props.urlMirrors}/>
: <ViewButton {...props}/>
: null;
};
interface IViewButtonGroupDropdownProps {
name: string;
url: string;
urlMirrors: string[];
}
const ViewButtonGroupDropdown = (props: IViewButtonGroupDropdownProps) => {
let firstButtonText: string = "Original";
let mirrorIndex: number = 0;
if (props.url.indexOf("web.archive.org") !== -1) {
firstButtonText = "Mirror 1";
mirrorIndex++;
}
return <div className="btn-group-vertical fl-btn-link" role="group">
<BtnGroupDropView/>
<div className="dropdown-menu" aria-labelledby="btnGroupDropView">
<ViewButton {...props} text={firstButtonText}/>
{props.urlMirrors.map(
(m, i) => <ViewButton {...props} url={m} text={`Mirror ${i + 1 + mirrorIndex}`}/>)}
</div>
</div>;
};
const BtnGroupDropView = () => {
return <button id="btnGroupDropView"
type="button"
className="btn btn-primary dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
View
</button>;
};

View file

@ -0,0 +1,5 @@
.fl-btn-link {
margin-bottom: .2rem;
max-width: 90px;
width: 90px;
}

View file

@ -0,0 +1,25 @@
import { ChatButton } from "./ChatButton";
import { DonateButton } from "./DonateButton";
import { EmailButton } from "./EmailButton";
import { ForumButton } from "./ForumButton";
import { HomeButton } from "./HomeButton";
import { IssuesButton } from "./IssuesButton";
import { PolicyButton } from "./PolicyButton";
import { SubmitButton } from "./SubmitButton";
import { SubscribeButtonGroup } from "./SubscribeButtonGroup";
import { TwitterButton } from "./TwitterButton";
import { ViewButtonGroup } from "./ViewButtonGroup";
export {
ChatButton,
DonateButton,
EmailButton,
ForumButton,
HomeButton,
IssuesButton,
PolicyButton,
SubmitButton,
SubscribeButtonGroup,
TwitterButton,
ViewButtonGroup
};

View file

@ -1,5 +1,3 @@
body, h2, h3, h4, h5, blockquote { font-size: 16px; }
.rt-tr {
-ms-align-items: center;
-o-align-items: center;
@ -7,16 +5,9 @@ body, h2, h3, h4, h5, blockquote { font-size: 16px; }
align-items: center;
}
div.rt-tbody, div.rt-thead.-filters, div.rt-thead.-header { min-width: 320px !important; }
.fl-tag-container {
line-height: 1;
white-space: normal;
}
div.rt-tbody, div.rt-thead.-filters, div.rt-thead.-header { min-width: 320px !important; }
.fl-btn-details-action {
margin-bottom: .2rem;
max-width: 90px;
}
.fl-description { margin: 0 0 .5rem; }
}

View file

@ -0,0 +1,5 @@
import { Home } from "./Home";
export {
Home
};

View file

@ -0,0 +1,5 @@
import { Home } from "./home";
export {
Home
};

View file

@ -1,8 +1,8 @@
import * as React from "react";
import { Layout } from "./Layout";
import { Route } from "react-router-dom";
import { Layout } from "./components/Layout";
import { Home } from "./components/Home";
import { Home } from "./modules";
export const routes = <Layout>
export const Routes = <Layout>
<Route exact path="/" component={ Home }/>
</Layout>;

View file

@ -0,0 +1 @@
body, h2, h3, h4, h5, blockquote { font-size: 16px; }

View file

@ -0,0 +1,8 @@
//https://stackoverflow.com/a/11868398/2343739
export const getContrast = (hexcolor: string) => {
const r = parseInt(hexcolor.substr(0, 2), 16);
const g = parseInt(hexcolor.substr(2, 2), 16);
const b = parseInt(hexcolor.substr(4, 2), 16);
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? "black" : "white";
}

View file

@ -0,0 +1,67 @@
/*https://projects.lukehaas.me/css-loaders/#load1*/
.loader,
.loader:before,
.loader:after {
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
background: #000000;
height: 4em;
width: 1em;
}
.loader {
-ms-transform: translateZ(0);
-webkit-animation-delay: -0.16s;
-webkit-transform: translateZ(0);
animation-delay: -0.16s;
color: #000000;
font-size: 11px;
margin: 88px auto;
position: relative;
text-indent: -9999em;
transform: translateZ(0);
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
left: -1.5em;
}
.loader:after { left: 1.5em; }
@-webkit-keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

View file

@ -9,7 +9,7 @@ module.exports = (env) => {
return [
{
stats: { modules: false },
entry: { 'main': "./ClientApp/boot.tsx" },
entry: { 'main': "./ClientApp/Boot.tsx" },
resolve: { extensions: [".js", ".jsx", ".ts", ".tsx"] },
output: {
path: path.join(__dirname, bundleOutputDir),