refactor Home component

This commit is contained in:
Collin M. Barrett 2018-09-15 11:46:30 -05:00
parent 691bab9ba6
commit f742992d74
32 changed files with 315 additions and 275 deletions

View file

@ -1,26 +1,10 @@
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 "./home.css";
import * as moment from "moment";
import { getContrast } from "../../utils/GetContrast";
import { DetailsExpanderContainer } from "./components";
interface IHomeState {
lists: IListDto[];
loadingLists: boolean;
ruleCount: number;
loadingRuleCount: boolean;
software: ISoftwareDto[];
loadingSoftware: boolean;
languages: ILanguageDto[];
loadingLanguages: boolean;
columnVisibility: IColumnVisibility[];
pageSize: number;
}
import { IListDto } from "./IListDto";
import { ISoftwareDto } from "./ISoftwareDto";
import { ILanguageDto } from "./ILanguageDto";
import { Tagline } from "./Tagline";
import { ListsTable } from "./ListsTable";
import { IColumnVisibility } from "./ListsTable";
const columnVisibilityDefaults = [
{ column: "Software", visible: true },
@ -29,74 +13,42 @@ const columnVisibilityDefaults = [
{ column: "Updated Date", visible: true }
];
export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
interface IProps {
lists: IListDto[];
ruleCount: number;
software: ISoftwareDto[];
languages: ILanguageDto[];
}
interface IState {
columnVisibility: IColumnVisibility[];
pageSize: number;
}
export class Home extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
lists: [],
loadingLists: true,
ruleCount: 0,
loadingRuleCount: true,
software: [],
loadingSoftware: true,
languages: [],
loadingLanguages: true,
columnVisibility: columnVisibilityDefaults,
pageSize: 20
};
this.updatePageSize = this.updatePageSize.bind(this);
}
render() {
const contents = this.state.loadingLists ||
this.state.loadingRuleCount ||
this.state.loadingSoftware ||
this.state.loadingLanguages
? <div className="loader">Loading...</div>
: <div>
{Home.renderTagline(this.state)}
{Home.renderFilterListsTable(this.state)}
{this.renderColumnVisibilityCheckboxes()}
</div>;
return <div>
{contents}
</div>;
}
componentDidMount() {
this.updatePageSize();
fetch("https://filterlists.com/api/v1/lists")
.then(response => response.json() as Promise<IListDto[]>)
.then(data => {
this.setState({
lists: data,
loadingLists: false
});
});
fetch("https://filterlists.com/api/v1/rules")
.then(response => response.json() as Promise<number>)
.then(data => {
this.setState({
ruleCount: data,
loadingRuleCount: false
});
});
fetch("https://filterlists.com/api/v1/software")
.then(response => response.json() as Promise<ISoftwareDto[]>)
.then(data => {
this.setState({
software: data,
loadingSoftware: false
});
});
fetch("https://filterlists.com/api/v1/languages")
.then(response => response.json() as Promise<ILanguageDto[]>)
.then(data => {
this.setState({
languages: data,
loadingLanguages: false
});
});
}
render() {
return this.props.lists
? <div>
<Tagline {...this.props} listCount={this.props.lists.length}/>
<ListsTable {...this.props}
pageSize={this.state.pageSize}
columnVisibility={this.state.columnVisibility}/>
{this.renderColumnVisibilityCheckboxes()}
</div>
: null;
}
updatePageSize() {
@ -105,168 +57,24 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
});
}
private static renderTagline(state: IHomeState) {
return <p className="ml-2 mr-2">
The independent, comprehensive directory of <strong>{state.ruleCount.toLocaleString()
}</strong> unique rules across <strong>{state.lists.length
}</strong> filter and host lists for advertisements, trackers, malware, and annoyances.
</p>;
}
private static renderFilterListsTable(state: IHomeState) {
return <ReactTable
data={state.lists}
key={state.pageSize}
defaultPageSize={state.pageSize}
showPageSizeOptions={false}
columns={[
{
Header: "Name",
accessor: "name",
filterable: true,
filterMethod: (filter: any, row: any) => row[filter.id].toUpperCase()
.includes(filter.value.toUpperCase()),
sortMethod: (a: any, b: any) => a.toUpperCase() > b.toUpperCase() ? 1 : -1,
Cell: (cell: any) => <h2 className="mb-0">{cell.value}</h2>,
style: { overflow: "visible" }
},
{
Header: "Software Support",
accessor: "softwareIds",
filterable: true,
filterMethod: (filter: any, row: any) => {
if (filter.value === "any") {
return true;
}
return row[filter.id].join(",").split(",").includes(filter.value);
},
Filter: ({ filter, onChange }) =>
<select
onChange={(event: any) => onChange(event.target.value)}
style={{ width: "100%" }}
value={filter ? filter.value : "any"}>
<option value="any">Any</option>
{state.software.map((e: any) => <option value={e.id}>{e.name}</option>)}
</select>,
sortable: false,
Cell: () => null,
width: 250,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: state.columnVisibility.filter((c: IColumnVisibility) => {
return c.column === "Software";
})[0].visible
},
{
Header: "Tags",
accessor: "tags",
filterable: true,
filterMethod: (filter: any, row: any) => row[filter.id].map((e: any) => e.name).join()
.toUpperCase().includes(filter.value.toUpperCase()),
sortable: false,
Cell: (cell: any) => <div className="fl-tag-container">{cell.value.map(
(e: any) => <span className="badge" style={{
backgroundColor: `#${e.colorHex}`,
color: getContrast(`${e.colorHex}`)
}} title={e.description}>{e.name}</span>)}</div>,
width: 200,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: state.columnVisibility.filter((c: IColumnVisibility) => {
return c.column === "Tags";
})[0].visible
},
{
Header: "Languages",
accessor: "languages",
filterable: true,
filterMethod: (filter: any, row: any) => {
if (filter.value === "any") {
return true;
}
return row[filter.id].map((x: any) => x.iso6391).includes(filter.value);
},
Filter: ({ filter, onChange }) =>
<select
onChange={(event: any) => onChange(event.target.value)}
style={{ width: "100%" }}
value={filter ? filter.value : "any"}>
<option value="any">Any</option>
{state.languages.map((e: any) => <option value={e.iso6391}>{e.name}</option>)}
</select>,
sortable: false,
Cell: (cell: any) => <div className="fl-tag-container">{cell.value.map(
(e: any) => <span className="badge badge-secondary" title={e.name}>{e.iso6391}</span>)
}</div>,
style: { whiteSpace: "inherit" },
width: 100,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: state.columnVisibility.filter((c: IColumnVisibility) => {
return c.column === "Languages";
})[0].visible
},
{
Header: "Updated",
accessor: "updatedDate",
filterable: true,
filterMethod: (filter: any, row: any) => row[filter.id].includes(filter.value),
sortMethod: (a: any, b: any) => moment(a).isValid()
? (moment(b).isValid() ? (moment(a).isBefore(b) ? -1 : 1) : 1)
: -1,
Cell: (cell: any) => <div>{moment(cell.value).isValid()
? moment(cell.value).format("l")
: "N/A"}</div>,
style: { whiteSpace: "inherit" },
width: 100,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: state.columnVisibility.filter((c: IColumnVisibility) => {
return c.column === "Updated Date";
})[0].visible
},
{
Header: "Details",
accessor: "id",
sortable: false,
expander: true,
Expander: ({ isExpanded, row }) =>
<div>
{isExpanded
? <button className="btn btn-primary btn-block active"
title={`Collapse details about ${row.name}.`}>
Details
</button>
: <button className="btn btn-primary btn-block"
title={`Learn more about ${row.name}.`}>
Details
</button>}
</div>,
style: { textAlign: "center" },
width: 90
}
]}
SubComponent={(row: any) => {
return (
<DetailsExpanderContainer listId={row.original.id}/>
);
}}
className="-striped -highlight"/>;
}
private renderColumnVisibilityCheckboxes() {
return <div className="d-none d-md-block text-right">
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map(
(c: IColumnVisibility) => this.renderColumnVisibilityCheckbox(c))}
(c: IColumnVisibility, i) => this.renderColumnVisibilityCheckbox(c, i))}
</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>
private renderColumnVisibilityCheckbox(props: IColumnVisibility, key: number) {
return <div className="form-check form-check-inline" key={key}>
<input className="form-check-input"
type="checkbox"
id={`checkbox${props.column.replace(/\s+/g, "")}`}
defaultChecked={props.visible}
onChange={() => this.checkColumn(props)}/>
<label className="form-check-label"
htmlFor={`checkbox${props.column.replace(/\s+/g, "")}`}>
{props.column}
</label>
</div>;
}
@ -285,39 +93,4 @@ export class Home extends React.Component<RouteComponentProps<{}>, IHomeState> {
}
return -1;
}
}
interface ISoftwareDto {
id: number;
name: string;
}
interface ILanguageDto {
iso6391: string;
name: string;
}
interface IListDto {
id: number;
name: string;
languages: IListLanguageDto[];
softwareIds: number[];
tags: IListTagDto[];
updatedDate: string;
}
interface IListLanguageDto {
name: string;
iso6391: string;
}
interface IListTagDto {
name: string;
colorHex: string;
description: string;
}
interface IColumnVisibility {
column: string;
visible: boolean;
}

View file

@ -0,0 +1,66 @@
import * as React from "react";
import "isomorphic-fetch";
import { IListDto } from "./IListDto";
import { ISoftwareDto } from "./ISoftwareDto";
import { ILanguageDto } from "./ILanguageDto";
import "../../utils/loader.css";
import { Home } from "./Home";
interface IState {
lists: IListDto[];
ruleCount: number;
software: ISoftwareDto[];
languages: ILanguageDto[];
}
export class HomeContainer extends React.Component<{}, IState> {
constructor(props: any) {
super(props);
this.state = {
lists: [],
ruleCount: 0,
software: [],
languages: []
};
}
componentDidMount() {
fetch("https://filterlists.com/api/v1/lists")
.then(r => r.json() as Promise<IListDto[]>)
.then(d => {
this.setState({
lists: d
});
});
fetch("https://filterlists.com/api/v1/rules")
.then(r => r.json() as Promise<number>)
.then(d => {
this.setState({
ruleCount: d
});
});
fetch("https://filterlists.com/api/v1/software")
.then(r => r.json() as Promise<ISoftwareDto[]>)
.then(d => {
this.setState({
software: d
});
});
fetch("https://filterlists.com/api/v1/languages")
.then(r => r.json() as Promise<ILanguageDto[]>)
.then(d => {
this.setState({
languages: d
});
});
}
render() {
return this.state.lists.length === 0 ||
this.state.ruleCount === 0 ||
this.state.software.length === 0 ||
this.state.languages.length === 0
? <div className="loader">Loading...</div>
: <Home {...this.state}/>;
}
}

View file

@ -0,0 +1,4 @@
export interface ILanguageDto {
iso6391: string;
name: string;
}

View file

@ -0,0 +1,19 @@
export interface IListDto {
id: number;
name: string;
languages: IListLanguageDto[];
softwareIds: number[];
tags: IListTagDto[];
updatedDate: string;
}
interface IListLanguageDto {
name: string;
iso6391: string;
}
interface IListTagDto {
name: string;
colorHex: string;
description: string;
}

View file

@ -0,0 +1,4 @@
export interface ISoftwareDto {
id: number;
name: string;
}

View file

@ -0,0 +1,159 @@
import * as React from "react";
import { IListDto } from "./IListDto";
import { ISoftwareDto } from "./ISoftwareDto";
import { ILanguageDto } from "./ILanguageDto";
import ReactTable from "react-table"
import "react-table/react-table.css"
import "./listsTable.css";
import * as moment from "moment";
import { getContrast } from "../../utils/GetContrast";
import { DetailsExpander } from "./components";
export interface IColumnVisibility {
column: string;
visible: boolean;
}
interface IProps {
lists: IListDto[];
software: ISoftwareDto[];
languages: ILanguageDto[];
columnVisibility: IColumnVisibility[];
pageSize: number;
}
export const ListsTable = (props: IProps) => {
return <ReactTable
data={props.lists}
key={props.pageSize}
defaultPageSize={props.pageSize}
showPageSizeOptions={false}
columns={[
{
Header: "Name",
accessor: "name",
filterable: true,
filterMethod: (f: any, r: any) =>
r[f.id].toUpperCase().includes(f.value.toUpperCase()),
sortMethod: (a: any, b: any) => a.toUpperCase() > b.toUpperCase() ? 1 : -1,
Cell: (c: any) => <h2 className="mb-0">{c.value}</h2>,
style: { overflow: "visible" }
},
{
Header: "Software Support",
accessor: "softwareIds",
filterable: true,
filterMethod: (f: any, r: any) =>
f.value === "any" || r[f.id].join(",").split(",").includes(f.value),
Filter: ({ filter, onChange }) =>
<select
onChange={(event: any) => onChange(event.target.value)}
style={{ width: "100%" }}
value={filter ? filter.value : "any"}>
<option value="any">Any</option>
{props.software.map((s: any, i) => <option value={s.id} key={i}>{s.name}</option>)}
</select>,
sortable: false,
Cell: () => null,
width: 250,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: props.columnVisibility.filter(
(c: IColumnVisibility) => { return c.column === "Software"; })[0].visible
},
{
Header: "Tags",
accessor: "tags",
filterable: true,
filterMethod: (f: any, r: any) =>
r[f.id].map((e: any) => e.name).join().toUpperCase().includes(f.value.toUpperCase()),
sortable: false,
Cell: (c: any) =>
<div className="fl-tag-container">
{c.value.map((e: any, i: any) => <span className="badge"
style={{
backgroundColor: `#${e.colorHex}`,
color: getContrast(`${e.colorHex}`)
}}
title={e.description}
key={i}>
{e.name}
</span>)}
</div>,
width: 200,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: props.columnVisibility.filter(
(c: IColumnVisibility) => { return c.column === "Tags"; })[0].visible
},
{
Header: "Languages",
accessor: "languages",
filterable: true,
filterMethod: (f: any, r: any) =>
f.value === "any" || r[f.id].map((x: any) => x.iso6391).includes(f.value),
Filter: ({ filter, onChange }) =>
<select
onChange={(event: any) => onChange(event.target.value)}
style={{ width: "100%" }}
value={filter ? filter.value : "any"}>
<option value="any">Any</option>
{props.languages.map((l: any, i) => <option value={l.iso6391} key={i}>{l.name}</option>)
}
</select>,
sortable: false,
Cell: (c: any) => <div className="fl-tag-container">
{c.value.map((e: any, i: any) => <span className="badge badge-secondary"
title={e.name}
key={i}>
{e.iso6391}
</span>)}
</div>,
style: { whiteSpace: "inherit" },
width: 100,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: props.columnVisibility.filter(
(c: IColumnVisibility) => { return c.column === "Languages"; })[0].visible
},
{
Header: "Updated",
accessor: "updatedDate",
filterable: true,
filterMethod: (f: any, r: any) => r[f.id].includes(f.value),
sortMethod: (a: any, b: any) =>
moment(a).isValid() ? (moment(b).isValid() ? (moment(a).isBefore(b) ? -1 : 1) : 1) : -1,
Cell: (c: any) => <div>
{moment(c.value).isValid() ? moment(c.value).format("l") : "N/A"}
</div>,
style: { whiteSpace: "inherit" },
width: 100,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: props.columnVisibility.filter(
(c: IColumnVisibility) => { return c.column === "Updated Date"; })[0].visible
},
{
Header: "Details",
accessor: "id",
sortable: false,
expander: true,
Expander: ({ isExpanded, row }) =>
<div>
{isExpanded
? <button className="btn btn-primary btn-block active"
title={`Collapse details about ${row.name}.`}>
Details
</button>
: <button className="btn btn-primary btn-block"
title={`Learn more about ${row.name}.`}>
Details
</button>}
</div>,
style: { textAlign: "center" },
width: 90
}
]}
SubComponent={(r: any) => <DetailsExpander listId={r.original.id}/>}
className="-striped -highlight"/>;
};

View file

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

View file

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

View file

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

View file

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