refactor api

This commit is contained in:
Collin Barrett 2018-09-24 18:59:28 -05:00
parent 3a8a01f4bc
commit be67e4cccb
29 changed files with 505 additions and 369 deletions

View file

@ -1,7 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6B3E6765_002D5B8B_002D4A23_002DAD12_002D7E2BC5F4D2E6_002Fd_003Awwwroot_002Fd_003Adist/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=_002A_002EDesigner_002Ecs/@EntryIndexedValue"></s:String>
<s:Boolean x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=_002A_002EDesigner_002Ecs/@EntryIndexRemoved">True</s:Boolean>
@ -59,7 +56,7 @@
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Test Methods" Priority="100"&gt;&#xD;
&lt;Entry Priority="100" DisplayName="Test Methods"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
@ -92,7 +89,7 @@
&lt;/Entry.Match&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="All other members" /&gt;&#xD;
&lt;Entry DisplayName="Test Methods" Priority="100"&gt;&#xD;
&lt;Entry Priority="100" DisplayName="Test Methods"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Method" /&gt;&#xD;
@ -105,7 +102,7 @@
&lt;/Entry&gt;&#xD;
&lt;/TypePattern&gt;&#xD;
&lt;TypePattern DisplayName="Default Pattern"&gt;&#xD;
&lt;Entry DisplayName="Public Delegates" Priority="100"&gt;&#xD;
&lt;Entry Priority="100" DisplayName="Public Delegates"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
@ -116,7 +113,7 @@
&lt;Name /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Public Enums" Priority="100"&gt;&#xD;
&lt;Entry Priority="100" DisplayName="Public Enums"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Access Is="Public" /&gt;&#xD;
@ -174,7 +171,7 @@
&lt;Access /&gt;&#xD;
&lt;/Entry.SortBy&gt;&#xD;
&lt;/Entry&gt;&#xD;
&lt;Entry DisplayName="Interface Implementations" Priority="100"&gt;&#xD;
&lt;Entry Priority="100" DisplayName="Interface Implementations"&gt;&#xD;
&lt;Entry.Match&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Kind Is="Member" /&gt;&#xD;
@ -195,6 +192,10 @@
&lt;/Patterns&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Wayback/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Waybacks/@EntryIndexedValue">True</s:Boolean>

View file

@ -5,7 +5,7 @@
namespace FilterLists.Api.V1.Controllers
{
[ApiVersion("1.0")]
[ApiVersion("1")]
//TODO: use versioning without needing to manually specify in swagger-ui (https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/370)
[Route("v{version:apiVersion}/[controller]")]
[ResponseCache(Duration = 86400)]

View file

@ -5,7 +5,7 @@ import { BrowserRouter } from "react-router-dom";
import * as RoutesModule from "./Routes";
let routes = RoutesModule.Routes;
function renderApp() {
const renderApp = () => {
const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")!;
ReactDOM.render(
<AppContainer>
@ -13,7 +13,7 @@ function renderApp() {
</AppContainer>,
document.getElementById("react-app")
);
}
};
renderApp();

View file

@ -6,58 +6,51 @@ interface IProps {
children?: React.ReactNode;
}
export const Layout = (props: IProps) => {
return <div className="container">
<Header/>
<div className="row">
<div className="w-100">
{ props.children }
</div>
</div>
<Footer/>
</div>;
};
export const Layout = (props: IProps) =>
<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 Header = () =>
<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 Footer = () =>
<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 ApiLink = () =>
<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 GitHubLink = () =>
<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 DonateLink = () =>
<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>;
};
const OwnerLink = () =>
<a href="https://collinmbarrett.com/">
Collin M. Barrett
</a>;

View file

@ -1,8 +1,8 @@
import * as React from "react";
import { IListDto, ISoftwareDto, ILanguageDto } from "./interfaces";
import { Tagline, ListsTable, IColumnVisibility } from "./components";
import { IColumnVisibility, ILanguage, IList, ISoftware, ITag } from "./interfaces";
import { Tagline, ListsTable } from "./components";
const columnVisibilityDefaults = [
const columnVisibilityDefaults: IColumnVisibility[] = [
{ column: "Software", visible: true },
{ column: "Tags", visible: true },
{ column: "Languages", visible: true },
@ -10,16 +10,17 @@ const columnVisibilityDefaults = [
];
interface IProps {
languages: ILanguageDto[];
lists: IListDto[];
languages: ILanguage[];
lists: IList[];
ruleCount: number;
software: ISoftwareDto[];
}
software: ISoftware[];
tags: ITag[];
};
interface IState {
columnVisibility: IColumnVisibility[];
pageSize: number;
}
};
export class Home extends React.Component<IProps, IState> {
constructor(props: IProps) {
@ -33,32 +34,32 @@ export class Home extends React.Component<IProps, IState> {
componentDidMount() {
this.updatePageSize();
}
render() {
return this.props.lists
? <div>
<Tagline listCount={this.props.lists.length} ruleCount={this.props.ruleCount}/>
<ListsTable {...this.props} {...this.state}/>
{this.renderColumnVisibilityCheckboxes()}
</div>
: null;
}
};
updatePageSize() {
this.setState({
pageSize: Math.max(Math.floor((window.innerHeight - 386) / 52), 5)
});
}
};
private renderColumnVisibilityCheckboxes() {
return <div className="d-none d-md-block text-right">
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map(
(c: IColumnVisibility, i) => this.renderColumnVisibilityCheckbox(c, i))}
render() {
return <div>
<Tagline listCount={this.props.lists.length} ruleCount={this.props.ruleCount}/>
<ListsTable {...this.props} {...this.state}/>
{this.renderColumnVisibilityCheckboxes()}
</div>;
};
private renderColumnVisibilityCheckbox(props: IColumnVisibility, key: number) {
renderColumnVisibilityCheckboxes() {
return this.props.lists.length > 0
? <div className="d-none d-md-block text-right">
Visible:&nbsp;&nbsp;{this.state.columnVisibility.map(
(c: IColumnVisibility, i: number) => this.renderColumnVisibilityCheckbox(c, i))}
</div>
: null;
};
renderColumnVisibilityCheckbox(props: IColumnVisibility, key: number) {
return <div className="form-check form-check-inline" key={key}>
<input className="form-check-input"
type="checkbox"
@ -70,21 +71,21 @@ export class Home extends React.Component<IProps, IState> {
{props.column}
</label>
</div>;
}
};
private checkColumn(props: IColumnVisibility) {
checkColumn(props: IColumnVisibility) {
const columnVisibility = this.state.columnVisibility;
const index = this.findWithAttr(columnVisibility, "column", props.column);
columnVisibility[index].visible = !columnVisibility[index].visible;
this.forceUpdate();
}
};
private findWithAttr(array: any, attr: string, value: string) {
findWithAttr(array: any, attr: string, value: string) {
for (let i = 0; i < array.length; i += 1) {
if (array[i][attr] === value) {
return i;
}
}
return -1;
}
}
};
};

View file

@ -1,64 +1,67 @@
import * as React from "react";
import "isomorphic-fetch";
import { ILanguageDto, IListDto, ISoftwareDto } from "./interfaces";
import "../../utils/loader.css";
import { ILanguage, IList, ISoftware, ITag } from "./interfaces";
import { Home } from "./Home";
interface IState {
languages: ILanguageDto[];
lists: IListDto[];
languages: ILanguage[];
lists: IList[];
ruleCount: number;
software: ISoftwareDto[];
}
software: ISoftware[];
tags: ITag[];
};
export class HomeContainer extends React.Component<{}, IState> {
constructor(props: any) {
super(props);
this.state = {
lists: [],
languages: [],
lists: [],
ruleCount: 0,
software: []
software: [],
tags: []
};
}
componentDidMount() {
fetch("https://filterlists.com/api/v1/lists")
.then(r => r.json() as Promise<IListDto[]>)
.then(d => {
this.setState({
lists: d
});
});
this.fetchLists();
this.fetchSoftware();
this.fetchTags();
this.fetchLanguages();
this.fetchRuleCount();
};
fetchLists() {
fetch("https://filterlists.com/api/v1/lists/alpha")
.then(r => r.json() as Promise<IList[]>)
.then(d => { this.setState({ lists: d }); });
};
fetchSoftware() {
fetch("https://filterlists.com/api/v1/software")
.then(r => r.json() as Promise<ISoftware[]>)
.then(d => { this.setState({ software: d }); });
};
fetchTags() {
fetch("https://filterlists.com/api/v1/tags")
.then(r => r.json() as Promise<ITag[]>)
.then(d => { this.setState({ tags: d }); });
};
fetchLanguages() {
fetch("https://filterlists.com/api/v1/languages")
.then(r => r.json() as Promise<ILanguage[]>)
.then(d => { this.setState({ languages: d }); });
};
fetchRuleCount() {
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
});
});
}
.then(d => { this.setState({ ruleCount: 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}/>;
}
}
return <Home {...this.state}/>;
};
};

View file

@ -0,0 +1,39 @@
import * as React from "react";
import { ITag } from "../interfaces";
import { getContrast } from "../../../utils";
export const Tag = (props: ITag) => {
const hexColor = kelly_colors_hex[props.id % kelly_colors_hex.length];
return <span className="badge"
style={{
backgroundColor: `#${hexColor}`,
color: getContrast(hexColor)
}}
title={props.description}>
{props.name}
</span>;
};
//https://stackoverflow.com/a/4382138/2343739
const kelly_colors_hex = [
"FFB300", // Vivid Yellow
"803E75", // Strong Purple
"FF6800", // Vivid Orange
"A6BDD7", // Very Light Blue
"C10020", // Vivid Red
"CEA262", // Grayish Yellow
"817066", // Medium Gray
"007D34", // Vivid Green
"F6768E", // Strong Purplish Pink
"00538A", // Strong Blue
"FF7A5C", // Strong Yellowish Pink
"53377A", // Strong Violet
"FF8E00", // Vivid Orange Yellow
"B32851", // Strong Purplish Red
"F4C800", // Vivid Greenish Yellow
"7F180D", // Strong Reddish Brown
"93AA00", // Vivid Yellowish Green
"593315", // Deep Yellowish Brown
"F13A13", // Vivid Reddish Orange
"232C16" // Dark Olive Green
];

View file

@ -2,14 +2,16 @@
interface IProps {
listCount: number;
ruleCount: number,
}
ruleCount: 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>;
};
export const Tagline = (props: IProps) =>
props.listCount > 0 && props.ruleCount > 0
? <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>
: <p className="ml-2 mr-2">
The independent, comprehensive directory of filter and host lists for advertisements, trackers, malware, and annoyances.
</p>;

View file

@ -1,11 +1,9 @@
import { DetailsExpander } from "./detailsExpander";
import { ListsTable } from "./listsTable";
import { IColumnVisibility } from "./listsTable";
import { Tagline } from "./Tagline";
export {
DetailsExpander,
ListsTable,
IColumnVisibility,
Tagline
};

View file

@ -1,183 +1,35 @@
import * as React from "react";
import { IListDto, ISoftwareDto, ILanguageDto } from "../../interfaces";
import { IColumnVisibility, ILanguage, IList, ISoftware, ITag } from "../../interfaces";
import "../../../../utils/loader.css";
import ReactTable from "react-table"
import "react-table/react-table.css"
import "./listsTable.css";
import { SoftwareIcon } from "../softwareIcon";
import { getContrast } from "../../../../utils";
import * as moment from "moment";
import { DetailsButton, Languages, Name, Software, Tags, UpdatedDate } from "./columns";
import { DetailsExpander } from "../../components";
export interface IColumnVisibility {
column: string;
visible: boolean;
}
interface IProps {
languages: ILanguageDto[];
lists: IListDto[];
software: ISoftwareDto[];
languages: ILanguage[];
lists: IList[];
software: ISoftware[];
tags: ITag[];
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>
},
{
Header: "Software",
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: ISoftwareDto, i: number) => <option value={s.id} key={i}>{s.name}</option>)}
</select>,
sortable: false,
Cell: (c: any) => c.value
? c.value.map((s: number, i: number) => <SoftwareIcon id={s} key={i}/>)
: null,
width: 155,
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]
? r[f.id].map((e: any) => e.name).join().toUpperCase().includes(f.value.toUpperCase())
: null),
sortMethod: (a: any, b: any) => a
? b
? a.length === b.length
? a[0].name > b[0].name
? -1
: 1
: a.length > b.length
? 1
: -1
: 1
: -1,
Cell: (c: any) => c.value
? <div className="fl-tag-container">
{c.value.map((e: any, i: number) =>
<span className="badge"
style={{
backgroundColor: `#${e.colorHex}`,
color: getContrast(`${e.colorHex}`)
}}
title={e.description}
key={i}>
{e.name}
</span>)}
</div>
: null,
width: 215,
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]
? r[f.id].map((x: any) => x.iso6391).includes(f.value)
: false),
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
? props.languages.map((l: ILanguageDto, i: number) =>
<option value={l.iso6391} key={i}>{l.name}</option>)
: null}
</select>,
sortable: false,
Cell: (c: any) => c.value
? <div className="fl-tag-container">
{c.value.map((e: ILanguageDto, i: number) =>
<span className="badge badge-secondary"
title={e.name}
key={i}>
{e.iso6391}
</span>)}
</div>
: null,
style: { whiteSpace: "inherit" },
width: 95,
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: false,
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"/>;
};
export const ListsTable = (props: IProps) =>
props.lists.length > 0 && props.languages.length > 0 && props.software.length > 0
? <ReactTable
data={props.lists}
defaultPageSize={props.pageSize}
showPageSizeOptions={false}
columns={[
Name,
Software(props.columnVisibility, props.software),
Tags(props.columnVisibility, props.tags),
Languages(props.columnVisibility, props.languages),
UpdatedDate(props.columnVisibility),
DetailsButton
]}
SubComponent={(r: any) => <DetailsExpander listId={r.original.id}/>}
className="-striped -highlight"/>
: <div className="loader">Loading...</div>;

View file

@ -0,0 +1,25 @@
import * as React from "react";
import { Column, RowRenderProps } from "react-table";
export const DetailsButton = {
Header: "Details",
accessor: "id",
sortable: false,
expander: true,
Expander: ({ isExpanded, row }: RowRenderProps) => Expander({ isExpanded, row }),
style: { textAlign: "center" },
width: 90
} as Column;
const Expander = (props: RowRenderProps) =>
<div>
{props.isExpanded
? <button className="btn btn-primary btn-block active"
title={`Collapse details about ${props.row.name}.`}>
Details
</button>
: <button className="btn btn-primary btn-block"
title={`Learn more about ${props.row.name}.`}>
Details
</button>}
</div>;

View file

@ -0,0 +1,52 @@
import * as React from "react";
import { Column, Filter } from "react-table";
import { IColumnVisibility, ILanguage } from "../../../interfaces";
export const Languages = (columnVisibility: IColumnVisibility[], languages: ILanguage[]) => {
return {
Header: "Languages",
accessor: "languageIds",
filterable: true,
filterMethod: (f: Filter, r: any[]) => filterMethod(f, r),
Filter: ({ onChange, filter }: any) => Filter({ onChange, filter }, languages),
sortable: false,
Cell: (c: any) => Cell(c.value, languages),
style: { whiteSpace: "inherit" },
width: 95,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: columnVisibility.filter((c: IColumnVisibility) => c.column === "Languages")[0].visible
} as Column;
};
const filterMethod = (f: Filter, r: any[]): boolean => {
const listLanguageIds = r[f.id as any];
return f.value === "any" ||
(listLanguageIds
? listLanguageIds.join(",").split(",").includes(f.value)
: false);
};
const Filter = (props: any, languages: ILanguage[]) =>
<select onChange={(event: any) => props.onChange(event.target.value)}
style={{ width: "100%" }}
value={props.filter ? props.filter.value : "any"}>
<option value="any">Any</option>
{languages.length > 0
? languages.map((l: ILanguage, i: number) => <option value={l.id} key={i}>{l.name}</option>)
: null}
</select>;
const Cell = (languageIds: number[], languages: ILanguage[]) =>
languageIds
? <div className="fl-tag-container">
{languageIds.map((id: number, i: number) => {
const language = languages.filter((l: ILanguage) => l.id === id)[0];
return <span className="badge badge-secondary"
title={language.name}
key={i}>
{language.iso6391}
</span>;
})}
</div>
: null;

View file

@ -0,0 +1,22 @@
import * as React from "react";
import { Column, Filter } from "react-table";
export const Name = {
Header: "Name",
accessor: "name",
filterable: true,
filterMethod: (f: Filter, r: any[]) => filterMethod(f, r),
sortMethod: (a: string, b: string) => sortMethod(a, b),
Cell: (c: any) => Cell(c.value)
} as Column;
const filterMethod = (f: Filter, r: any[]): boolean =>
r[f.id as any].toUpperCase().includes(f.value.toUpperCase());
const sortMethod = (a: string, b: string) =>
a.toUpperCase() > b.toUpperCase() ? 1 : -1;
const Cell = (name: string) =>
name
? <h2 className="mb-0">{name}</h2>
: null;

View file

@ -0,0 +1,46 @@
import * as React from "react";
import { Column, Filter } from "react-table";
import { IColumnVisibility, ISoftware } from "../../../interfaces";
import { SoftwareIcon } from "../../softwareIcon";
export const Software = (columnVisibility: IColumnVisibility[], software: ISoftware[]) => {
return {
Header: "Software",
accessor: "syntaxId",
filterable: true,
filterMethod: (f: Filter, r: any[]) => filterMethod(f, r, software),
Filter: ({ filter, onChange }: any) => Filter({ onChange, filter }, software),
sortable: false,
Cell: (c: any) => Cell(c.value, software),
width: 155,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: columnVisibility.filter((c: IColumnVisibility) => c.column === "Software")[0].visible
} as Column;
};
const filterMethod = (f: Filter, r: any[], software: ISoftware[]): boolean => {
const isAny = f.value === "any";
const softwareFiltered = isAny ? software : software.filter((s: ISoftware) => s.id === parseInt(f.value));
const listSyntaxId: number = r[f.id as any];
const isMatch = softwareFiltered[0].syntaxIds ? softwareFiltered[0].syntaxIds.indexOf(listSyntaxId) > -1 : false;
return isAny || (listSyntaxId ? isMatch : false);
};
const Filter = (props: any, software: ISoftware[]) =>
<select
onChange={(event: any) => props.onChange(event.target.value)}
style={{ width: "100%" }}
value={props.filter ? props.filter.value : "any"}>
<option value="any">Any</option>
{software.length > 0
? software.map(
(s: ISoftware, i: number) => <option value={s.id} key={i}>{s.name}</option>)
: null}
</select>;
const Cell = (listSyntaxId: number, software: ISoftware[]) =>
listSyntaxId
? software.filter((s: ISoftware) => s.syntaxIds.indexOf(listSyntaxId) > -1)
.map((s: ISoftware, i: number) => <SoftwareIcon id={s.id} key={i}/>)
: null;

View file

@ -0,0 +1,49 @@
import * as React from "react";
import { Column, Filter } from "react-table";
import { Tag } from "../../Tag"
import { IColumnVisibility, ITag } from "../../../interfaces";
export const Tags = (columnVisibility: IColumnVisibility[], tags: ITag[]) => {
return {
Header: "Tags",
accessor: "tagIds",
filterable: true,
filterMethod: (f: Filter, r: any[]) => filterMethod(f, r, tags),
sortMethod: (a: number[], b: number[]) => sortMethod(a, b),
Cell: (c: any) => Cell(c.value, tags),
width: 215,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: columnVisibility.filter((c: IColumnVisibility) => c.column === "Tags")[0].visible
} as Column;
};
const filterMethod = (f: Filter, r: any[], tags: ITag[]): boolean => {
const listTagIds = r[f.id as any];
if (f.value.length > 0 && listTagIds && listTagIds.length > 0) {
const listTags = tags.filter((t: ITag) => listTagIds.includes(t.id));
const listTagNames = listTags.map((t: ITag) => t.name.toLowerCase()).join(",").split(",");
return listTagNames.filter((l: string) => l.indexOf(f.value.toLowerCase()) > -1).length > 0;
} else {
return false;
}
};
const sortMethod = (a: number[], b: number[]): any =>
a
? b
? a.length > b.length
? 1
: -1
: 1
: -1;
const Cell = (tagIds: number[], tags: ITag[]) =>
tagIds
? <div className="fl-tag-container">
{tagIds.map((tid: number, i: number) => {
const tag = tags.filter((t: ITag) => t.id === tid)[0];
return <Tag {...tag} key={i}/>;
})}
</div>
: null;

View file

@ -0,0 +1,34 @@
import * as React from "react";
import { Column } from "react-table";
import * as moment from "moment";
import { IColumnVisibility } from "../../../interfaces";
export const UpdatedDate = (columnVisibility: IColumnVisibility[]) => {
return {
Header: "Updated",
accessor: "updatedDate",
sortMethod: (a: string, b: string) => sortMethod(a, b),
Cell: (c: any) => Cell(c.value),
style: { whiteSpace: "inherit" },
width: 100,
headerClassName: "d-none d-md-block",
className: "d-none d-md-block",
show: columnVisibility.filter((c: IColumnVisibility) => c.column === "Updated Date")[0].visible
} as Column;
};
const sortMethod = (a: string, b: string) =>
moment(a).isValid()
? (moment(b).isValid()
? (moment(a).isBefore(b)
? -1
: 1)
: 1)
: -1;
const Cell = (updatedDate: string) =>
<div>
{moment(updatedDate).isValid()
? moment(updatedDate).format("l")
: null}
</div>;

View file

@ -0,0 +1,15 @@
import { DetailsButton } from "./DetailsButton";
import { Languages } from "./Languages";
import { Name } from "./Name";
import { Software } from "./Software";
import { Tags } from "./Tags";
import { UpdatedDate } from "./UpdatedDate";
export {
DetailsButton,
Languages,
Name,
Software,
Tags,
UpdatedDate
};

View file

@ -1,6 +1,5 @@
import { ListsTable, IColumnVisibility } from "./ListsTable";
import { ListsTable } from "./ListsTable";
export {
ListsTable,
IColumnVisibility
};

View file

@ -0,0 +1,4 @@
export interface IColumnVisibility {
column: string;
visible: boolean;
};

View file

@ -0,0 +1,5 @@
export interface ILanguage {
id: number;
iso6391: string;
name: string;
}

View file

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

View file

@ -0,0 +1,8 @@
export interface IList {
id: number;
languageIds: number[];
name: string;
syntaxId: number;
tagIds: number[];
updatedDate: string;
}

View file

@ -1,19 +0,0 @@
export interface IListDto {
id: number;
languages: IListLanguageDto[];
name: string;
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,5 @@
export interface ISoftware {
id: number;
name: string;
syntaxIds: number[];
}

View file

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

View file

@ -0,0 +1,5 @@
export interface ITag {
id: number;
description: string;
name: string;
}

View file

@ -1,9 +1,14 @@
import { ILanguageDto } from "./ILanguageDto";
import { IListDto } from "./IListDto";
import { ISoftwareDto } from "./ISoftwareDto";
import { IColumnVisibility } from "./IColumnVisibility";
import { ILanguage } from "./ILanguage";
import { IList } from "./IList";
import { ISoftware } from "./ISoftware";
import { ITag } from "./ITag";
export {
ILanguageDto,
IListDto,
ISoftwareDto
IColumnVisibility,
ILanguage,
IList,
ISoftware,
ITag
};

View file

@ -5,4 +5,4 @@ export const getContrast = (hexcolor: string) => {
const b = parseInt(hexcolor.substr(4, 2), 16);
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? "black" : "white";
}
};

View file

@ -85,7 +85,7 @@
"@types/webpack-env": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz",
"integrity": "sha1-Eo0WhafDTTHtFwEPyH1qEsHeaXY=",
"integrity": "sha512-5Th3OsZ4gTRdr9Mho83BQ23cex4sRhOR4XTG+m+cJc0FhtUBK9Vn62hBJ+pnQYnSxoPOsKoAPOx6FcphxBC8ng==",
"dev": true
},
"acorn": {
@ -496,7 +496,7 @@
"bootstrap": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz",
"integrity": "sha1-DrNxryyESOjCEEEdDLgkpkCaEr4=",
"integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w==",
"dev": true
},
"brace-expansion": {
@ -1735,7 +1735,7 @@
"extract-text-webpack-plugin": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz",
"integrity": "sha1-XwQ+qgL5dQqSWLeMCm4NwUCPsvc=",
"integrity": "sha512-bt/LZ4m5Rqt/Crl2HiKuAl/oqg0psx1tsTLkvWbJen1CtD+fftkZhMaQ9HOtY2gWsl2Wq+sABmMVi9z3DhKWQQ==",
"dev": true,
"requires": {
"async": "^2.4.1",
@ -2892,7 +2892,7 @@
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
"integrity": "sha1-IrXH8xYzxbgCHH9KipVKwTnujVs=",
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
"dev": true,
"requires": {
"invariant": "^2.2.1",
@ -2927,7 +2927,7 @@
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
"integrity": "sha1-xZA89AnA39kI84jmGdhrnBF0y0c=",
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==",
"dev": true
},
"hosted-git-info": {
@ -3059,7 +3059,7 @@
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dev": true,
"requires": {
"loose-envify": "^1.0.0"
@ -3297,7 +3297,7 @@
"jquery": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
"integrity": "sha1-lYzinoHJeQ8xvneS311NlfxX+8o=",
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==",
"dev": true
},
"js-base64": {
@ -3331,7 +3331,7 @@
"json-loader": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
"integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=",
"integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==",
"dev": true
},
"json-schema-traverse": {
@ -4829,7 +4829,7 @@
"react-router": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
"integrity": "sha1-qtpK7xTICcsuaGsFzuR0IjRQbE4=",
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==",
"dev": true,
"requires": {
"history": "^4.7.2",
@ -4844,7 +4844,7 @@
"react-router-dom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
"integrity": "sha1-TCYZ/CTE+ofJ/Rj0+0pD/mP71cY=",
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
"dev": true,
"requires": {
"history": "^4.7.2",
@ -5021,7 +5021,7 @@
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk=",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==",
"dev": true
},
"resolve-url": {
@ -6123,7 +6123,7 @@
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c=",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==",
"dev": true
},
"vendors": {
@ -6144,7 +6144,7 @@
"warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.1.tgz",
"integrity": "sha1-Zs43a3+/6KiHwivfDnNJ1z05d0U=",
"integrity": "sha512-rAVtTNZw+cQPjvGp1ox0XC5Q2IBFyqoqh+QII4J/oguyu83Bax1apbo2eqB8bHRS+fqYUBagys6lqUoVwKSmXQ==",
"dev": true,
"requires": {
"loose-envify": "^1.0.0"