mirror of
https://github.com/collinbarrett/FilterLists.git
synced 2026-03-11 09:04:27 +00:00
refactor GUI (primarily detailsExpander)
This commit is contained in:
parent
8cbd3e05fb
commit
1854689ce9
54 changed files with 962 additions and 542 deletions
|
|
@ -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"><?xml version="1.0" encoding="utf-16"?>
|
||||
<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
|
||||
<TypePattern DisplayName="Non-reorderable types">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
63
src/FilterLists.Web/ClientApp/Layout.tsx
Normal file
63
src/FilterLists.Web/ClientApp/Layout.tsx
Normal 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>;
|
||||
};
|
||||
8
src/FilterLists.Web/ClientApp/Routes.tsx
Normal file
8
src/FilterLists.Web/ClientApp/Routes.tsx
Normal 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>;
|
||||
|
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
|
@ -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)}&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)}&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)}&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)}&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)}&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;
|
||||
};
|
||||
|
|
@ -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: {this.state.columnVisibility.map((c: IColumnVisibility) => this.renderColumnVisibilityCheckbox(c))}
|
||||
Visible: {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>;
|
||||
}
|
||||
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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>;;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { DetailsExpanderContainer } from "./DetailsExpanderContainer";
|
||||
|
||||
export {
|
||||
DetailsExpanderContainer
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.fl-description { margin: 0 0 .5rem; }
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { InfoCard } from "./InfoCard";
|
||||
|
||||
export {
|
||||
InfoCard
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { MaintainersInfoCard } from "./MaintainersInfoCard";
|
||||
|
||||
export {
|
||||
MaintainersInfoCard
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { DetailsExpanderContainer } from "./detailsExpanderContainer";
|
||||
|
||||
export {
|
||||
DetailsExpanderContainer
|
||||
};
|
||||
|
|
@ -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;
|
||||
};;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 = `&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;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.fl-btn-link {
|
||||
margin-bottom: .2rem;
|
||||
max-width: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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; }
|
||||
}
|
||||
5
src/FilterLists.Web/ClientApp/modules/home/index.ts
Normal file
5
src/FilterLists.Web/ClientApp/modules/home/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Home } from "./Home";
|
||||
|
||||
export {
|
||||
Home
|
||||
};
|
||||
5
src/FilterLists.Web/ClientApp/modules/index.ts
Normal file
5
src/FilterLists.Web/ClientApp/modules/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Home } from "./home";
|
||||
|
||||
export {
|
||||
Home
|
||||
};
|
||||
|
|
@ -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>;
|
||||
1
src/FilterLists.Web/ClientApp/site.css
Normal file
1
src/FilterLists.Web/ClientApp/site.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
body, h2, h3, h4, h5, blockquote { font-size: 16px; }
|
||||
8
src/FilterLists.Web/ClientApp/utils/GetContrast.ts
Normal file
8
src/FilterLists.Web/ClientApp/utils/GetContrast.ts
Normal 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";
|
||||
}
|
||||
67
src/FilterLists.Web/ClientApp/utils/loader.css
Normal file
67
src/FilterLists.Web/ClientApp/utils/loader.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue