diff --git a/directory/web/.dockerignore b/directory/web/.dockerignore new file mode 100644 index 000000000..77fa549de --- /dev/null +++ b/directory/web/.dockerignore @@ -0,0 +1,5 @@ +**/Dockerfile +**/.dockerignore +**/.gitignore +**/azure-pipelines.yaml +**/node_modules \ No newline at end of file diff --git a/directory/web/.env b/directory/web/.env new file mode 100644 index 000000000..2d2e9a0cd --- /dev/null +++ b/directory/web/.env @@ -0,0 +1 @@ +INLINE_RUNTIME_CHUNK=false \ No newline at end of file diff --git a/directory/web/.gitignore b/directory/web/.gitignore new file mode 100644 index 000000000..821ef96d4 --- /dev/null +++ b/directory/web/.gitignore @@ -0,0 +1,25 @@ +# https://raw.githubusercontent.com/facebook/create-react-app/master/packages/cra-template/template/gitignore + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/directory/web/Dockerfile b/directory/web/Dockerfile new file mode 100644 index 000000000..6c9f46bbd --- /dev/null +++ b/directory/web/Dockerfile @@ -0,0 +1,26 @@ +# init base +FROM nginx:alpine as base +COPY reverse-proxy/conf.d /etc/nginx/conf.d + +# init install +FROM node:alpine as install + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#node-gyp-alpine +RUN apk add --no-cache python make g++ + +# install +WORKDIR /usr/src/app/ +COPY package*.json ./ +RUN npm ci --only=production + +# init build +FROM node:alpine as build +COPY --from=install node_modules . + +# build +COPY . . +RUN npm run build + +# final +FROM base as final +COPY --from=build /usr/src/app/build /usr/share/nginx/html \ No newline at end of file diff --git a/directory/web/config-overrides.js b/directory/web/config-overrides.js new file mode 100644 index 000000000..b2a41e367 --- /dev/null +++ b/directory/web/config-overrides.js @@ -0,0 +1,9 @@ +const { override, fixBabelImports } = require("customize-cra"); + +module.exports = override( + fixBabelImports("import", { + libraryName: "antd", + libraryDirectory: "es", + style: "css", + }) +); diff --git a/directory/web/package-lock.json b/directory/web/package-lock.json new file mode 100644 index 000000000..f4b640e7d Binary files /dev/null and b/directory/web/package-lock.json differ diff --git a/directory/web/package.json b/directory/web/package.json new file mode 100644 index 000000000..0f518f8a7 --- /dev/null +++ b/directory/web/package.json @@ -0,0 +1,68 @@ +{ + "name": "filterlists.web", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^10.4.9", + "@testing-library/user-event": "^12.1.2", + "@types/jest": "^26.0.10", + "@types/node": "^14.6.0", + "@types/react": "^16.9.46", + "@types/react-dom": "^16.9.8", + "@types/react-router-dom": "^5.1.5", + "antd": "^4.6.1", + "babel-plugin-import": "^1.13.0", + "customize-cra": "^1.0.0", + "react": "^16.13.1", + "react-app-rewired": "^2.1.6", + "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0", + "react-scripts": "^3.4.3", + "slugify": "^1.4.5", + "typescript": "^4.0.2" + }, + "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-scripts eject" + }, + "proxy": "https://filterlists.com", + "eslintConfig": { + "extends": [ + "react-app", + "plugin:prettier/recommended" + ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "**/*.{json,js,jsx,ts,tsx,css,scss,yaml,md}": [ + "prettier --write", + "git add" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "husky": "^4.2.5", + "lint-staged": "^10.2.11", + "prettier": "^2.0.5" + } +} diff --git a/directory/web/public/icon_filterlists.png b/directory/web/public/icon_filterlists.png new file mode 100644 index 000000000..99c15bdde Binary files /dev/null and b/directory/web/public/icon_filterlists.png differ diff --git a/directory/web/public/index.html b/directory/web/public/index.html new file mode 100644 index 000000000..c59925ee6 --- /dev/null +++ b/directory/web/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + FilterLists | Subscriptions for uBlock Origin, Adblock Plus, AdGuard, ... + + + + + + +
+ + + diff --git a/directory/web/public/logo_filterlists.png b/directory/web/public/logo_filterlists.png new file mode 100644 index 000000000..7d8d18170 Binary files /dev/null and b/directory/web/public/logo_filterlists.png differ diff --git a/directory/web/public/manifest.json b/directory/web/public/manifest.json new file mode 100644 index 000000000..452af93c5 --- /dev/null +++ b/directory/web/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "FilterLists", + "name": "FilterLists | Subscriptions for uBlock Origin, Adblock Plus, AdGuard, ...", + "icons": [ + { + "src": "icon_filterlists.png", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/png" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/directory/web/public/robots.txt b/directory/web/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/directory/web/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/directory/web/reverse-proxy/conf.d/default.conf b/directory/web/reverse-proxy/conf.d/default.conf new file mode 100644 index 000000000..173de90d8 --- /dev/null +++ b/directory/web/reverse-proxy/conf.d/default.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /index.html; + } + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/directory/web/src/App.test.tsx b/directory/web/src/App.test.tsx new file mode 100644 index 000000000..5e49ca508 --- /dev/null +++ b/directory/web/src/App.test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +import { App } from "./App"; + +it("renders without crashing", () => { + const div = document.createElement("div"); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/directory/web/src/App.tsx b/directory/web/src/App.tsx new file mode 100644 index 000000000..c39af99a3 --- /dev/null +++ b/directory/web/src/App.tsx @@ -0,0 +1,158 @@ +import { + ApiOutlined, + DollarOutlined, + GithubOutlined, + TeamOutlined, + TwitterOutlined, +} from "@ant-design/icons"; +import { Layout, Menu, Tag } from "antd"; +import React from "react"; +import { + BrowserRouter as Router, + Link, + Route, + RouteComponentProps, + Switch, +} from "react-router-dom"; + +import { ListsTable } from "./components"; + +const { Header, Content, Footer } = Layout; + +export const App: React.FC = () => ( + + +
+ + +
+ +
+ + + + +
+
+
+ + + + + + +
+
+
+); + +const Logo = () => ( + + FilterLists logo + +); + +const NotFound = (props: RouteComponentProps) => ( +

+ 404 Not Found: {props.location.pathname} +

+); + +const CopyrightAuthor = () => ( + + ©{new Date().getFullYear()}  + + Collin M. Barrett + + +); + +const Twitter = () => ( + + + Twitter + + +); + +const Community = () => ( + + + Community + + +); + +const GitHub = () => ( + + + GitHub + + +); + +const Api = () => ( + + + API + + +); + +const Donate = () => ( + + + Donate + + +); diff --git a/directory/web/src/components/Description.tsx b/directory/web/src/components/Description.tsx new file mode 100644 index 000000000..1a0471cf6 --- /dev/null +++ b/directory/web/src/components/Description.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +interface Props { + description: string; + descriptionSourceUrl: string; +} + +export const Description = (props: Props) => + props.descriptionSourceUrl ? ( +
+ {props.description} +
+ ) : ( +

{props.description}

+ ); diff --git a/directory/web/src/components/LicenseTag.tsx b/directory/web/src/components/LicenseTag.tsx new file mode 100644 index 000000000..8beb0e554 --- /dev/null +++ b/directory/web/src/components/LicenseTag.tsx @@ -0,0 +1,32 @@ +import { Tag } from "antd"; +import * as React from "react"; + +interface Props { + name: string; + descriptionUrl?: string; + showLabel?: boolean; +} + +export const LicenseTag = (props: Props) => + props.name ? ( + + {props.showLabel &&

License:

} + + + +
+ ) : null; + +const TagContents = (props: Props) => + props.descriptionUrl ? ( + + {props.name} + + ) : ( + <>{props.name} + ); diff --git a/directory/web/src/components/LinkButton.tsx b/directory/web/src/components/LinkButton.tsx new file mode 100644 index 000000000..7a3fe5e82 --- /dev/null +++ b/directory/web/src/components/LinkButton.tsx @@ -0,0 +1,27 @@ +import { Button } from "antd"; +import { ButtonType } from "antd/lib/button"; +import React from "react"; + +interface Props { + url: string; + text: string; + title?: string; + type?: ButtonType; + icon?: React.ReactNode; +} + +export const LinkButton = (props: Props) => + props.url && props.text ? ( + + ) : null; diff --git a/directory/web/src/components/ListInfoButton.tsx b/directory/web/src/components/ListInfoButton.tsx new file mode 100644 index 000000000..37013b331 --- /dev/null +++ b/directory/web/src/components/ListInfoButton.tsx @@ -0,0 +1,28 @@ +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Button } from "antd"; +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; +import slugify from "slugify"; + +import { SlugifyOptions } from "../constants"; + +interface Props { + listName: string; +} + +export const ListInfoButton = (props: RouteComponentProps & Props) => { + const listPath = `/lists/${slugify(props.listName, SlugifyOptions)}`; + return ( + + ); +}; diff --git a/directory/web/src/components/PublishedDate.tsx b/directory/web/src/components/PublishedDate.tsx new file mode 100644 index 000000000..c8b9d6c3c --- /dev/null +++ b/directory/web/src/components/PublishedDate.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +interface Props { + publishedDate: string; +} + +export const PublishedDate = (props: Props) => + props.publishedDate ? ( + <> +

First Published Date:

+ {new Date(props.publishedDate).toDateString()} + + ) : null; diff --git a/directory/web/src/components/SubscribeButtons.tsx b/directory/web/src/components/SubscribeButtons.tsx new file mode 100644 index 000000000..461951ab7 --- /dev/null +++ b/directory/web/src/components/SubscribeButtons.tsx @@ -0,0 +1,104 @@ +import { ImportOutlined } from "@ant-design/icons"; +import { Button } from "antd"; +import { ButtonType } from "antd/lib/button"; +import React from "react"; + +interface Props { + name: string; + viewUrl: string; + viewUrlMirrors: string[]; +} + +export const SubscribeButtons = (props: Props) => + props.viewUrl ? ( + <> + + + + ) : null; + +interface MirrorButtonsProps { + name: string; + viewUrlMirrors: string[]; +} + +const MirrorButtons = (props: MirrorButtonsProps) => ( + <> + {props.viewUrlMirrors && props.viewUrlMirrors.length + ? props.viewUrlMirrors.map((viewUrlMirror: string, i: number) => ( + + )) + : null} + +); + +interface SubscribeButtonProps { + name: string; + viewUrl: string; + text: string; +} + +const SubscribeButton = (props: SubscribeButtonProps) => { + const buttonProps = buildButtonProps(props.name, props.viewUrl); + return ( + + ); +}; + +const buildButtonProps = (name: string, viewUrl: string) => { + let type: ButtonType = "primary"; + let disabled: boolean = false; + + const hrefLocation = `${encodeURIComponent(viewUrl)}`; + const hrefTitle = `${encodeURIComponent(name)}`; + let href = `abp:subscribe?location=${hrefLocation}&title=${hrefTitle}`; + + let prefixes: string[] = []; + let message = `Subscribe to ${name} with a browser extension supporting the "abp:" protocol (e.g. uBlock Origin, Adblock Plus).`; + + // HTTP protocols + if (viewUrl.includes(".onion/")) { + type = "dashed"; + prefixes.push("TOR"); + } + if (viewUrl.includes("http://")) { + type = "dashed"; + prefixes.push("INSECURE"); + } + + // Software protocols + if (viewUrl.includes(".tpl")) { + disabled = true; // IE not supported by FilterLists + } + if ( + viewUrl.includes(".lsrules") || + viewUrl.includes("?hostformat=littlesnitch") + ) { + href = `x-littlesnitch:subscribe-rules?url=${hrefLocation}`; + message = `Subscribe to ${name} with Little Snitch's rule group subscription feature.`; + } + + const title = `${ + prefixes.length ? prefixes.join(" | ") + " | " : "" + }${message}`; + + return { disabled, type, href, title }; +}; diff --git a/directory/web/src/components/SyntaxTag.tsx b/directory/web/src/components/SyntaxTag.tsx new file mode 100644 index 000000000..4c04f91b6 --- /dev/null +++ b/directory/web/src/components/SyntaxTag.tsx @@ -0,0 +1,32 @@ +import { Tag } from "antd"; +import * as React from "react"; + +interface Props { + name: string; + definitionUrl?: string; + showLabel?: boolean; +} + +export const SyntaxTag = (props: Props) => + props.name ? ( + + {props.showLabel &&

Syntax:

} + + + +
+ ) : null; + +const TagContents = (props: Props) => + props.definitionUrl ? ( + + {props.name} + + ) : ( + <>{props.name} + ); diff --git a/directory/web/src/components/index.ts b/directory/web/src/components/index.ts new file mode 100644 index 000000000..059e95e97 --- /dev/null +++ b/directory/web/src/components/index.ts @@ -0,0 +1,3 @@ +import { ListsTable } from "./listsTable"; + +export { ListsTable }; diff --git a/directory/web/src/components/languageCloud/LanguageCloud.module.css b/directory/web/src/components/languageCloud/LanguageCloud.module.css new file mode 100644 index 000000000..879bc4070 --- /dev/null +++ b/directory/web/src/components/languageCloud/LanguageCloud.module.css @@ -0,0 +1,7 @@ +.grow { + /* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */ + max-height: none !important; + + /* leave vertical gap between rows of tags */ + line-height: 28px; +} diff --git a/directory/web/src/components/languageCloud/LanguageCloud.tsx b/directory/web/src/components/languageCloud/LanguageCloud.tsx new file mode 100644 index 000000000..d6e6c8cfe --- /dev/null +++ b/directory/web/src/components/languageCloud/LanguageCloud.tsx @@ -0,0 +1,24 @@ +import { Tag } from "antd"; +import React from "react"; + +import { Language } from "../../interfaces/Language"; +import styles from "./LanguageCloud.module.css"; + +interface Props { + languages: Language[]; + showLabel?: boolean; +} + +export const LanguageCloud = (props: Props) => + props.languages && props.languages.length ? ( +
+ {props.showLabel && ( +

{`Language${props.languages.length > 1 ? "s" : ""}:`}

+ )} + {props.languages.map((l: Language, i: number) => ( + + {l.iso6391} + + ))} +
+ ) : null; diff --git a/directory/web/src/components/languageCloud/index.ts b/directory/web/src/components/languageCloud/index.ts new file mode 100644 index 000000000..c0509230a --- /dev/null +++ b/directory/web/src/components/languageCloud/index.ts @@ -0,0 +1,3 @@ +import { LanguageCloud } from "./LanguageCloud"; + +export { LanguageCloud }; diff --git a/directory/web/src/components/listInfoDrawer/ListInfoDrawer.tsx b/directory/web/src/components/listInfoDrawer/ListInfoDrawer.tsx new file mode 100644 index 000000000..9a80c9b4b --- /dev/null +++ b/directory/web/src/components/listInfoDrawer/ListInfoDrawer.tsx @@ -0,0 +1,156 @@ +import "./listInfoDrawer.css"; + +import { Divider, Drawer } from "antd"; +import { + DollarOutlined, + FileExclamationOutlined, + FormOutlined, + GithubOutlined, + HomeOutlined, + MailOutlined, + MessageOutlined, + SearchOutlined, + TeamOutlined, +} from "@ant-design/icons"; +import React, { useEffect, useState } from "react"; + +import ButtonGroup from "antd/lib/button/button-group"; +import { Description } from "../Description"; +import { Language } from "../../interfaces/Language"; +import { LanguageCloud } from "../languageCloud"; +import { License } from "../../interfaces/License"; +import { LicenseTag } from "../LicenseTag"; +import { LinkButton } from "../LinkButton"; +import { List } from "../../interfaces/List"; +import { Maintainer } from "../../interfaces/Maintainer"; +import { Maintainers } from "../maintainers"; +import { PublishedDate } from "../PublishedDate"; +import { RouteComponentProps } from "react-router-dom"; +import { Software } from "../../interfaces/Software"; +import { SoftwareCloud } from "../softwareCloud"; +import { SubscribeButtons } from "../SubscribeButtons"; +import { Syntax } from "../../interfaces/Syntax"; +import { SyntaxTag } from "../SyntaxTag"; +import { Tag } from "../../interfaces/Tag"; +import { TagCloud } from "../tagCloud"; + +interface Props { + list: List; + languages: Language[]; + license: License | undefined; + maintainers: Maintainer[]; + software: Software[]; + syntax: Syntax | undefined; + tags: Tag[]; +} + +export const ListInfoDrawer = (props: RouteComponentProps & Props) => { + const [originalTitle] = useState(document.title); + useEffect(() => { + const updateTitle = () => + (document.title = props.list.name + " | " + originalTitle); + updateTitle(); + return () => { + document.title = originalTitle; + }; + }, [props.list, originalTitle]); + return ( + props.history.push("/")} + > + + + + {props.license && ( + + )} + {props.syntax && ( + + )} + + + + + + + } + /> + } + /> + } + /> + {props.list.emailAddress && ( + } + /> + )} + } + /> + } + /> + } + /> + } + /> + } + /> + + + ); +}; diff --git a/directory/web/src/components/listInfoDrawer/index.ts b/directory/web/src/components/listInfoDrawer/index.ts new file mode 100644 index 000000000..03a1cedaf --- /dev/null +++ b/directory/web/src/components/listInfoDrawer/index.ts @@ -0,0 +1,3 @@ +import { ListInfoDrawer } from "./ListInfoDrawer"; + +export { ListInfoDrawer }; diff --git a/directory/web/src/components/listInfoDrawer/listInfoDrawer.css b/directory/web/src/components/listInfoDrawer/listInfoDrawer.css new file mode 100644 index 000000000..fb65e5d6a --- /dev/null +++ b/directory/web/src/components/listInfoDrawer/listInfoDrawer.css @@ -0,0 +1,4 @@ +h3 { + margin-top: 1em; + margin-bottom: 0.2em; +} diff --git a/directory/web/src/components/listsTable/ListDrawer.tsx b/directory/web/src/components/listsTable/ListDrawer.tsx new file mode 100644 index 000000000..7c23b7074 --- /dev/null +++ b/directory/web/src/components/listsTable/ListDrawer.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { + Redirect, + Route, + RouteComponentProps, + StaticContext, +} from "react-router"; + +import { Language } from "../../interfaces/Language"; +import { License } from "../../interfaces/License"; +import { List } from "../../interfaces/List"; +import { Maintainer } from "../../interfaces/Maintainer"; +import { Software } from "../../interfaces/Software"; +import { Syntax } from "../../interfaces/Syntax"; +import { Tag } from "../../interfaces/Tag"; +import { ListInfoDrawer } from "../listInfoDrawer"; + +interface Props { + lists: List[]; + languages: Language[]; + licenses: License[]; + maintainers: Maintainer[]; + software: Software[]; + syntaxes: Syntax[]; + tags: Tag[]; +} + +export const ListDrawer = (props: Props) => { + const renderDrawer = (rp: RouteComponentProps) => { + const list = props.lists.find((l) => l.slug === rp.match.params.listSlug); + return list ? ( + + list.languageIds.includes(l.id) + ) + } + license={ + list.licenseId + ? props.licenses.find((l: License) => list.licenseId === l.id) + : props.licenses.find((l: License) => l.id === 5) + } + maintainers={ + list.maintainerIds && + props.maintainers.filter((m: Maintainer) => + list.maintainerIds.includes(m.id) + ) + } + software={ + list.syntaxId + ? props.software.filter((s: Software) => + s.syntaxIds.includes(list.syntaxId) + ) + : [] + } + syntax={ + list.syntaxId + ? props.syntaxes.find((s) => s.id === list.syntaxId) + : undefined + } + tags={ + list.tagIds && + props.tags.filter((t: Tag) => list.tagIds.includes(t.id)) + } + {...rp} + /> + ) : props.lists && props.lists.length ? ( + + ) : null; + }; + return ; +}; diff --git a/directory/web/src/components/listsTable/ListsTable.module.css b/directory/web/src/components/listsTable/ListsTable.module.css new file mode 100644 index 000000000..f0e8cd19c --- /dev/null +++ b/directory/web/src/components/listsTable/ListsTable.module.css @@ -0,0 +1,24 @@ +.nogrow > * { + /* limit table cells from wrapping more than 2 lines */ + /* https://stackoverflow.com/a/13924997/2343739 */ + /* https://stackoverflow.com/a/22042054/2343739 */ + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box !important; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; /* number of lines to show */ + line-height: 1.5; /* fallback */ + max-height: 3em; /* fallback */ + + /* fixed row height */ + min-height: 3em; + + /* fix left-side truncation of Description column on narrow viewports in Chrome */ + word-break: break-word; +} + +.nogrow { + /* eliminate top and bottom padding in cells to fit more rows per page */ + padding-top: 0px !important; + padding-bottom: 0px !important; +} diff --git a/directory/web/src/components/listsTable/ListsTable.tsx b/directory/web/src/components/listsTable/ListsTable.tsx new file mode 100644 index 000000000..c1f9af73f --- /dev/null +++ b/directory/web/src/components/listsTable/ListsTable.tsx @@ -0,0 +1,386 @@ +import "./listsTable.css"; + +import { + Key, + SorterResult, + TableCurrentDataSource, +} from "antd/lib/table/interface"; +import React, { useEffect, useState } from "react"; +import { SoftwareCloud, SoftwareIcon } from "../softwareCloud"; +import { Table, Tag } from "antd"; +import { useSearchColumnFilter, useTablePageSizer } from "../../hooks"; + +import { Description } from "../Description"; +import { Language } from "../../interfaces/Language"; +import { LanguageCloud } from "../languageCloud"; +import { License } from "../../interfaces/License"; +import { LicenseTag } from "../LicenseTag"; +import { List } from "../../interfaces/List"; +import { ListInfoButton } from "../ListInfoButton"; +import { Maintainer } from "../../interfaces/Maintainer"; +import { MaintainerCloud } from "../maintainerCloud"; +import { RouteComponentProps } from "react-router"; +import { Software } from "../../interfaces/Software"; +import { Syntax } from "../../interfaces/Syntax"; +import { SyntaxTag } from "../SyntaxTag"; +import { TagCloud } from "../tagCloud"; +import { Tag as TagInterface } from "../../interfaces/Tag"; +import { arraySorter } from "./arraySorter"; +import { nameof } from "../../utils"; +import styles from "./ListsTable.module.css"; +import { TablePaginationConfig } from "antd/lib/table"; + +interface Props { + lists: List[]; + languages: Language[]; + licenses: License[]; + maintainers: Maintainer[]; + software: Software[]; + syntaxes: Syntax[]; + tags: TagInterface[]; +} + +export const ListsTable = (props: RouteComponentProps & Props) => { + const { + lists, + languages, + licenses, + maintainers, + software, + syntaxes, + tags, + ...routeComponentProps + } = props; + const tablePageSize = useTablePageSizer(); + const searchNameColumn = useSearchColumnFilter(nameof("name")); + const searchDescriptionColumn = useSearchColumnFilter( + nameof("description") + ); + const [visibleLists, setVisibleLists] = useState(lists); + useEffect(() => { + setVisibleLists(lists); + }, [lists]); + return ( + + dataSource={lists} + rowKey={(record) => record.id.toString()} + loading={lists.length ? false : true} + size="small" + pagination={{ + simple: true, + style: { float: "left", margin: "4px 4px" }, + pageSize: tablePageSize.pageSize, + }} + scroll={{ + x: + tablePageSize.isNarrowWindow || tablePageSize.isWideWindow + ? undefined + : 1892, + }} + onChange={( + _pagination: TablePaginationConfig, + _filters: Record, + _sorter: SorterResult | SorterResult[], + extra: TableCurrentDataSource + ) => setVisibleLists(extra.currentDataSource)} + > + + title="Info" + key="Info" + dataIndex={nameof("id")} + width={62} + className={styles.nogrow} + fixed={tablePageSize.isNarrowWindow ? undefined : "left"} + render={(_id: number, list: List) => ( + + )} + /> + + title="Name" + key="Name" + dataIndex={nameof("name")} + sorter={(a, b) => a.name.localeCompare(b.name)} + defaultSortOrder={"ascend"} + width={tablePageSize.isNarrowWindow ? undefined : 200} + className={styles.nogrow} + fixed={tablePageSize.isNarrowWindow ? undefined : "left"} + filterDropdown={searchNameColumn.filterDropdown} + filterIcon={searchNameColumn.filterIcon} + onFilter={searchNameColumn.onFilter} + render={(name: string) =>
{name}
} + /> + {tablePageSize.isNarrowWindow ? null : ( + + title="Description" + key="Description" + dataIndex={nameof("description")} + sorter={(a, b) => a.description.localeCompare(b.description)} + width={ + !tablePageSize.isNarrowWindow && !tablePageSize.isWideWindow + ? 423 + : undefined + } + className={styles.nogrow} + filterDropdown={searchDescriptionColumn.filterDropdown} + filterIcon={searchDescriptionColumn.filterIcon} + onFilter={searchDescriptionColumn.onFilter} + render={(description: string, list: List) => ( + + )} + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="Software" + key="Software" + dataIndex={nameof("syntaxId")} + sorter={(a, b) => { + const getSoftwareIds = (l: List) => + software + .filter( + (s: Software) => + s.syntaxIds && s.syntaxIds.includes(l.syntaxId) + ) + .map((s) => s.id); + return arraySorter(getSoftwareIds(a), getSoftwareIds(b), software); + }} + width={170} + className={styles.nogrow} + filters={software.map((s) => ({ + text: ( + <> + +   + {s.name}  ( + { + visibleLists.filter( + (l) => s.syntaxIds && s.syntaxIds.includes(l.syntaxId) + ).length + } + ) + + ), + value: s.name, + }))} + onFilter={(value, record) => + software + .filter((s: Software) => s.name === value) + .flatMap((s) => s.syntaxIds) + .includes(record.syntaxId) + } + render={(syntaxId: number) => + syntaxId ? ( + s.syntaxIds && s.syntaxIds.includes(syntaxId) + )} + /> + ) : null + } + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="Syntax" + key="Syntax" + dataIndex={nameof("syntaxId")} + sorter={(a, b) => { + const syntaxA = syntaxes.find((s) => s.id === a.syntaxId); + const syntaxB = syntaxes.find((s) => s.id === b.syntaxId); + return syntaxA + ? syntaxB + ? syntaxA.name.localeCompare(syntaxB.name) + : -1 + : 1; + }} + width={254} + className={styles.nogrow} + filters={syntaxes.map((s) => ({ + text: ( + <> + ( + { + visibleLists.filter((l) => l.syntaxId && l.syntaxId === s.id) + .length + } + ) + + ), + value: s.id.toString(), + }))} + onFilter={(value, record) => + record.syntaxId ? record.syntaxId.toString() === value : false + } + render={(syntaxId: number) => { + const syntax = syntaxes.find((s) => s.id === syntaxId); + return syntax ? ( + + ) : null; + }} + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="Languages" + key="Languages" + dataIndex={nameof("languageIds")} + sorter={(a, b) => + arraySorter(a.languageIds, b.languageIds, languages) + } + width={129} + className={styles.nogrow} + filters={languages.map((l) => ({ + text: ( + <> + {l.iso6391}  + {l.name}  ( + { + visibleLists.filter( + (li) => li.languageIds && li.languageIds.includes(l.id) + ).length + } + ) + + ), + value: l.id.toString(), + }))} + onFilter={(value, record) => + record.languageIds ? record.languageIds.includes(+value) : false + } + render={(languageIds: number[]) => + languageIds ? ( + + languageIds.includes(l.id) + )} + /> + ) : null + } + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="Tags" + key="Tags" + dataIndex={nameof("tagIds")} + sorter={(a, b) => arraySorter(a.tagIds, b.tagIds, tags)} + width={275} + className={styles.nogrow} + filters={tags.map((t) => ({ + text: ( + <> + {t.name}( + { + visibleLists.filter( + (l) => l.tagIds && l.tagIds.includes(t.id) + ).length + } + ) + + ), + value: t.id.toString(), + }))} + onFilter={(value, record) => + record.tagIds ? record.tagIds.includes(+value) : false + } + render={(tagIds: number[]) => + tagIds ? ( + tagIds.includes(t.id))} + /> + ) : null + } + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="Maintainers" + key="Maintainers" + dataIndex={nameof("maintainerIds")} + sorter={(a, b) => + arraySorter(a.maintainerIds, b.maintainerIds, maintainers) + } + width={191} + className={styles.nogrow} + filters={maintainers.map((t) => ({ + text: ( + <> + {t.name}( + { + visibleLists.filter( + (l) => l.maintainerIds && l.maintainerIds.includes(t.id) + ).length + } + ) + + ), + value: t.id.toString(), + }))} + onFilter={(value, record) => + record.maintainerIds ? record.maintainerIds.includes(+value) : false + } + render={(maintainerIds: number[]) => + maintainerIds ? ( + + maintainerIds.includes(t.id) + )} + /> + ) : null + } + /> + )} + {tablePageSize.isNarrowWindow ? null : ( + + title="License" + key="License" + dataIndex={nameof("licenseId")} + sorter={(a, b) => { + const licenseA = licenses.find((s) => s.id === a.licenseId); + const licenseB = licenses.find((s) => s.id === b.licenseId); + return licenseA + ? licenseB + ? licenseA.name.localeCompare(licenseB.name) + : -1 + : 1; + }} + width={215} + className={styles.nogrow} + filters={licenses.map((l) => ({ + text: ( + <> + ( + { + visibleLists.filter( + (li) => li.licenseId && li.licenseId === l.id + ).length + } + ) + + ), + value: l.id.toString(), + }))} + onFilter={(value, record) => + record.licenseId ? record.licenseId.toString() === value : false + } + render={(licenseId: number) => { + const license = licenses.find((l) => l.id === licenseId); + return license ? ( + + ) : null; + }} + /> + )} + + ); +}; diff --git a/directory/web/src/components/listsTable/ListsTableHoc.tsx b/directory/web/src/components/listsTable/ListsTableHoc.tsx new file mode 100644 index 000000000..6c4b4c72a --- /dev/null +++ b/directory/web/src/components/listsTable/ListsTableHoc.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; + +import { + useLanguages, + useLicenses, + useLists, + useMaintainers, + useSoftware, + useSyntaxes, + useTags, +} from "../../hooks"; +import { ListDrawer } from "./ListDrawer"; +import { ListsTable } from "./ListsTable"; + +export const ListsTableHoc = (props: RouteComponentProps) => { + const lists = useLists(); + const languages = useLanguages(); + const licenses = useLicenses(); + const maintainers = useMaintainers(); + const software = useSoftware(); + const syntaxes = useSyntaxes(); + const tags = useTags(); + return ( + <> + + + + ); +}; diff --git a/directory/web/src/components/listsTable/arraySorter.ts b/directory/web/src/components/listsTable/arraySorter.ts new file mode 100644 index 000000000..17a5a8a62 --- /dev/null +++ b/directory/web/src/components/listsTable/arraySorter.ts @@ -0,0 +1,30 @@ +interface ArraySortableEntity { + id: number; + name: string; +} + +export const arraySorter = ( + a: number[], + b: number[], + entities: ArraySortableEntity[] +) => + a && a.length + ? b && b.length + ? a.length === b.length + ? entities + .filter((e: ArraySortableEntity) => a.includes(e.id)) + .map((e: ArraySortableEntity) => e.name) + .join() + .toLowerCase() > + entities + .filter((e: ArraySortableEntity) => b.includes(e.id)) + .map((e: ArraySortableEntity) => e.name) + .join() + .toLowerCase() + ? 1 + : -1 + : a.length > b.length + ? -1 + : 1 + : -1 + : 1; diff --git a/directory/web/src/components/listsTable/index.ts b/directory/web/src/components/listsTable/index.ts new file mode 100644 index 000000000..541bf2eb9 --- /dev/null +++ b/directory/web/src/components/listsTable/index.ts @@ -0,0 +1,3 @@ +import { ListsTableHoc } from "./ListsTableHoc"; + +export { ListsTableHoc as ListsTable }; diff --git a/directory/web/src/components/listsTable/listsTable.css b/directory/web/src/components/listsTable/listsTable.css new file mode 100644 index 000000000..34d395832 --- /dev/null +++ b/directory/web/src/components/listsTable/listsTable.css @@ -0,0 +1,3 @@ +.ant-table-header-column > div { + margin-top: 8px; +} diff --git a/directory/web/src/components/maintainerCloud/MaintainerCloud.module.css b/directory/web/src/components/maintainerCloud/MaintainerCloud.module.css new file mode 100644 index 000000000..879bc4070 --- /dev/null +++ b/directory/web/src/components/maintainerCloud/MaintainerCloud.module.css @@ -0,0 +1,7 @@ +.grow { + /* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */ + max-height: none !important; + + /* leave vertical gap between rows of tags */ + line-height: 28px; +} diff --git a/directory/web/src/components/maintainerCloud/MaintainerCloud.tsx b/directory/web/src/components/maintainerCloud/MaintainerCloud.tsx new file mode 100644 index 000000000..155b92fe1 --- /dev/null +++ b/directory/web/src/components/maintainerCloud/MaintainerCloud.tsx @@ -0,0 +1,31 @@ +import { Tag } from "antd"; +import React from "react"; + +import { Maintainer } from "../../interfaces/Maintainer"; +import styles from "./MaintainerCloud.module.css"; + +interface Props { + maintainers: Maintainer[]; +} + +export const MaintainerCloud = (props: Props) => + props.maintainers && props.maintainers.length ? ( +
+ {props.maintainers.map((m: Maintainer, i: number) => + m.homeUrl ? ( + + + {m.name} + + + ) : ( + {m.name} + ) + )} +
+ ) : null; diff --git a/directory/web/src/components/maintainerCloud/index.ts b/directory/web/src/components/maintainerCloud/index.ts new file mode 100644 index 000000000..c0c593fa3 --- /dev/null +++ b/directory/web/src/components/maintainerCloud/index.ts @@ -0,0 +1,3 @@ +import { MaintainerCloud } from "./MaintainerCloud"; + +export { MaintainerCloud }; diff --git a/directory/web/src/components/maintainers/Maintainers.tsx b/directory/web/src/components/maintainers/Maintainers.tsx new file mode 100644 index 000000000..ee51c764a --- /dev/null +++ b/directory/web/src/components/maintainers/Maintainers.tsx @@ -0,0 +1,67 @@ +import "./maintainers.css"; + +import { HomeOutlined, MailOutlined, TwitterOutlined } from "@ant-design/icons"; + +import { Card } from "antd"; +import React from "react"; + +import { Maintainer } from "../../interfaces/Maintainer"; + +interface Props { + maintainers: Maintainer[]; +} + +export const Maintainers = (props: Props) => + props.maintainers && props.maintainers.length ? ( + <> +

Maintainer{props.maintainers.length > 1 ? "s" : ""}:

+ {props.maintainers.map((m: Maintainer, index: number) => ( + + ))} + + ) : null; + +interface MaintainerComponentProps { + maintainer: Maintainer; +} + +const MaintainerComponent = (props: MaintainerComponentProps) => { + let actions = []; + if (props.maintainer.homeUrl) { + actions.push( + + + + ); + } + if (props.maintainer.emailAddress) { + actions.push( + + + + ); + } + if (props.maintainer.twitterHandle) { + actions.push( + + + + ); + } + return ; +}; diff --git a/directory/web/src/components/maintainers/index.ts b/directory/web/src/components/maintainers/index.ts new file mode 100644 index 000000000..c44a20b8f --- /dev/null +++ b/directory/web/src/components/maintainers/index.ts @@ -0,0 +1,3 @@ +import { Maintainers } from "./Maintainers"; + +export { Maintainers }; diff --git a/directory/web/src/components/maintainers/maintainers.css b/directory/web/src/components/maintainers/maintainers.css new file mode 100644 index 000000000..a4c829226 --- /dev/null +++ b/directory/web/src/components/maintainers/maintainers.css @@ -0,0 +1,3 @@ +.ant-card-body { + display: none; +} diff --git a/directory/web/src/components/softwareCloud/SoftwareCloud.module.css b/directory/web/src/components/softwareCloud/SoftwareCloud.module.css new file mode 100644 index 000000000..5daff62ad --- /dev/null +++ b/directory/web/src/components/softwareCloud/SoftwareCloud.module.css @@ -0,0 +1,4 @@ +/* allow for 2nd row's bottom border to not be chopped in AllListsTable */ +.grow { + max-height: none !important; +} diff --git a/directory/web/src/components/softwareCloud/SoftwareCloud.tsx b/directory/web/src/components/softwareCloud/SoftwareCloud.tsx new file mode 100644 index 000000000..332a23f3c --- /dev/null +++ b/directory/web/src/components/softwareCloud/SoftwareCloud.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +import { Software } from "../../interfaces/Software"; +import styles from "./SoftwareCloud.module.css"; +import { SoftwareIcon } from "./SoftwareIcon"; + +interface Props { + software: Software[]; + showLabel?: boolean; +} + +export const SoftwareCloud = (props: Props) => + props.software && props.software.length ? ( +
+ {props.showLabel &&

{`Software:`}

} + {props.software.map((s: Software, i: number) => + s.homeUrl ? ( + + + + ) : ( + + ) + )} +
+ ) : null; diff --git a/directory/web/src/components/softwareCloud/SoftwareIcon.tsx b/directory/web/src/components/softwareCloud/SoftwareIcon.tsx new file mode 100644 index 000000000..cef779a1c --- /dev/null +++ b/directory/web/src/components/softwareCloud/SoftwareIcon.tsx @@ -0,0 +1,110 @@ +import * as React from "react"; + +import { + img01, + img02, + img03, + img04, + img05, + img06, + img07, + img08, + img10, + img11, + img12, + img13, + img14, + img15, + img16, + img17, + img18, + img19, + img20, + img21, + img22, + img23, + img24, + img25, + img26, + img27, + img28, + img29, + img30, + img31, + img32, + img33, + img34, + img35, + img36, + img37, + img38, + img39, + img40, + img41, + img42, + img43, +} from "./imgs"; + +interface Props { + id: number; +} + +export const SoftwareIcon = (props: Props) => + icons[props.id] ? ( + {icons[props.id].imageTitle} + ) : null; + +interface Icon { + image: any; + imageTitle: string; +} + +const icons: { [id: number]: Icon } = { + 1: { image: img01, imageTitle: "uBlock Origin" }, + 2: { image: img02, imageTitle: "Adblock Plus" }, + 3: { image: img03, imageTitle: "AdGuard" }, + 4: { image: img04, imageTitle: "DNS66" }, + 5: { image: img05, imageTitle: "Nano Adblocker" }, + 6: { image: img06, imageTitle: "AdBlock" }, + 7: { image: img07, imageTitle: "AdAway" }, + 8: { image: img08, imageTitle: "Personal Blocklist" }, + 10: { image: img10, imageTitle: "Redirector" }, + 11: { image: img11, imageTitle: "Hosts File Editor" }, + 12: { image: img12, imageTitle: "Gas Mask" }, + 13: { image: img13, imageTitle: "MinerBlock" }, + 14: { image: img14, imageTitle: "Pi-hole" }, + 15: { image: img15, imageTitle: "uBlock" }, + 16: { image: img16, imageTitle: "Internet Explorer (TPL)" }, + 17: { image: img17, imageTitle: "Google Hit Hider by Domain" }, + 18: { image: img18, imageTitle: "FireHOL" }, + 19: { image: img19, imageTitle: "Samsung Knox" }, + 20: { image: img20, imageTitle: "Little Snitch" }, + 21: { image: img21, imageTitle: "Privoxy" }, + 22: { image: img22, imageTitle: "Diversion" }, + 23: { image: img23, imageTitle: "dnsmasq" }, + 24: { image: img24, imageTitle: "Slimjet" }, + 25: { image: img25, imageTitle: "uMatrix" }, + 26: { image: img26, imageTitle: "Blokada" }, + 27: { image: img27, imageTitle: "hostsmgr" }, + 28: { image: img28, imageTitle: "personalDNSfilter" }, + 29: { image: img29, imageTitle: "Unbound" }, + 30: { image: img30, imageTitle: "BIND" }, + 31: { image: img31, imageTitle: "AdGuard Home" }, + 32: { image: img32, imageTitle: "AdNauseam" }, + 33: { image: img33, imageTitle: "Legacy Unix derivatives" }, + 34: { image: img34, imageTitle: "Windows command line" }, + 35: { image: img35, imageTitle: "Shadowsocks" }, + 36: { image: img36, imageTitle: "ShadowsocksR" }, + 37: { image: img37, imageTitle: "Shadowrocket" }, + 38: { image: img38, imageTitle: "DNSRedirector" }, + 39: { image: img39, imageTitle: "pfBlockerNG" }, + 40: { image: img40, imageTitle: "Opera's built-in adblocker" }, + 41: { image: img41, imageTitle: "Surge" }, + 42: { image: img42, imageTitle: "dnscrypt-proxy" }, + 43: { image: img43, imageTitle: "SmartDNS" }, +}; diff --git a/directory/web/src/components/softwareCloud/imgs/01-uBlock-Origin.svg b/directory/web/src/components/softwareCloud/imgs/01-uBlock-Origin.svg new file mode 100644 index 000000000..03e7a9ebb --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/01-uBlock-Origin.svg @@ -0,0 +1 @@ + diff --git a/directory/web/src/components/softwareCloud/imgs/02-Adblock-Plus.svg b/directory/web/src/components/softwareCloud/imgs/02-Adblock-Plus.svg new file mode 100644 index 000000000..71ab1252f --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/02-Adblock-Plus.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/directory/web/src/components/softwareCloud/imgs/03-AdGuard.svg b/directory/web/src/components/softwareCloud/imgs/03-AdGuard.svg new file mode 100644 index 000000000..b9cb8ebd4 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/03-AdGuard.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/directory/web/src/components/softwareCloud/imgs/04-DNS66.png b/directory/web/src/components/softwareCloud/imgs/04-DNS66.png new file mode 100644 index 000000000..a32dc2c2f Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/04-DNS66.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/05-Nano-Adblocker.png b/directory/web/src/components/softwareCloud/imgs/05-Nano-Adblocker.png new file mode 100644 index 000000000..cd2b2a06d Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/05-Nano-Adblocker.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/06-AdBlock.png b/directory/web/src/components/softwareCloud/imgs/06-AdBlock.png new file mode 100644 index 000000000..84b1d1195 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/06-AdBlock.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/07-AdAway.png b/directory/web/src/components/softwareCloud/imgs/07-AdAway.png new file mode 100644 index 000000000..0826b7478 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/07-AdAway.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/08-Personal-Blocklist.png b/directory/web/src/components/softwareCloud/imgs/08-Personal-Blocklist.png new file mode 100644 index 000000000..9b6888a1b Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/08-Personal-Blocklist.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/10-Redirector.png b/directory/web/src/components/softwareCloud/imgs/10-Redirector.png new file mode 100644 index 000000000..33fa33b01 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/10-Redirector.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/11-Hosts-File-Editor.png b/directory/web/src/components/softwareCloud/imgs/11-Hosts-File-Editor.png new file mode 100644 index 000000000..e758cf5da Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/11-Hosts-File-Editor.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/12-Gas-Mask.png b/directory/web/src/components/softwareCloud/imgs/12-Gas-Mask.png new file mode 100644 index 000000000..062dcbd76 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/12-Gas-Mask.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/13-MinerBlock.svg b/directory/web/src/components/softwareCloud/imgs/13-MinerBlock.svg new file mode 100644 index 000000000..a2c2b0064 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/13-MinerBlock.svg @@ -0,0 +1,34 @@ + + + + + + Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + \ No newline at end of file diff --git a/directory/web/src/components/softwareCloud/imgs/14-Pi-hole.svg b/directory/web/src/components/softwareCloud/imgs/14-Pi-hole.svg new file mode 100644 index 000000000..3d3bd57f7 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/14-Pi-hole.svg @@ -0,0 +1 @@ +NewVortex diff --git a/directory/web/src/components/softwareCloud/imgs/15-uBlock.svg b/directory/web/src/components/softwareCloud/imgs/15-uBlock.svg new file mode 100644 index 000000000..6488ebb04 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/15-uBlock.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/directory/web/src/components/softwareCloud/imgs/16-Internet-Explorer-TPL.svg b/directory/web/src/components/softwareCloud/imgs/16-Internet-Explorer-TPL.svg new file mode 100644 index 000000000..69780795f --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/16-Internet-Explorer-TPL.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/directory/web/src/components/softwareCloud/imgs/17-Google-Hit-Hider-by-Domain.png b/directory/web/src/components/softwareCloud/imgs/17-Google-Hit-Hider-by-Domain.png new file mode 100644 index 000000000..ca830def3 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/17-Google-Hit-Hider-by-Domain.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/18-FireHOL.png b/directory/web/src/components/softwareCloud/imgs/18-FireHOL.png new file mode 100644 index 000000000..5db30bb01 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/18-FireHOL.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/19-Samsung-Knox.png b/directory/web/src/components/softwareCloud/imgs/19-Samsung-Knox.png new file mode 100644 index 000000000..bdb2f96a1 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/19-Samsung-Knox.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/20-Little-Snitch.png b/directory/web/src/components/softwareCloud/imgs/20-Little-Snitch.png new file mode 100644 index 000000000..1562eec0a Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/20-Little-Snitch.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/21-Privoxy.png b/directory/web/src/components/softwareCloud/imgs/21-Privoxy.png new file mode 100644 index 000000000..0ba2b59a1 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/21-Privoxy.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/22-Diversion.png b/directory/web/src/components/softwareCloud/imgs/22-Diversion.png new file mode 100644 index 000000000..511c8c8cd Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/22-Diversion.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/23-dnsmasq.png b/directory/web/src/components/softwareCloud/imgs/23-dnsmasq.png new file mode 100644 index 000000000..99e986ba9 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/23-dnsmasq.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/24-Slimjet.png b/directory/web/src/components/softwareCloud/imgs/24-Slimjet.png new file mode 100644 index 000000000..00f899268 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/24-Slimjet.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/25-uMatrix.png b/directory/web/src/components/softwareCloud/imgs/25-uMatrix.png new file mode 100644 index 000000000..82663fc9e Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/25-uMatrix.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/26-Blokada.png b/directory/web/src/components/softwareCloud/imgs/26-Blokada.png new file mode 100644 index 000000000..cfd376114 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/26-Blokada.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/27-hostsmgr.png b/directory/web/src/components/softwareCloud/imgs/27-hostsmgr.png new file mode 100644 index 000000000..349d5b17e Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/27-hostsmgr.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/28-personalDNSfilter.svg b/directory/web/src/components/softwareCloud/imgs/28-personalDNSfilter.svg new file mode 100644 index 000000000..90fd8f734 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/28-personalDNSfilter.svg @@ -0,0 +1,20 @@ + + + +Layer 1 + + + + + + + + + + + + \ No newline at end of file diff --git a/directory/web/src/components/softwareCloud/imgs/29-Unbound.png b/directory/web/src/components/softwareCloud/imgs/29-Unbound.png new file mode 100644 index 000000000..e6100dd8a Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/29-Unbound.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/30-BIND.png b/directory/web/src/components/softwareCloud/imgs/30-BIND.png new file mode 100644 index 000000000..74723afaf Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/30-BIND.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.png b/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.png new file mode 100644 index 000000000..e217b6879 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.svg b/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.svg new file mode 100644 index 000000000..d053374c3 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/31-AdGuard-Home.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/directory/web/src/components/softwareCloud/imgs/32-AdNauseam.png b/directory/web/src/components/softwareCloud/imgs/32-AdNauseam.png new file mode 100644 index 000000000..773f3c556 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/32-AdNauseam.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/33-Legacy-Unix-Derivatives.png b/directory/web/src/components/softwareCloud/imgs/33-Legacy-Unix-Derivatives.png new file mode 100644 index 000000000..617ecf5bd Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/33-Legacy-Unix-Derivatives.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/34-Windows-command-line.png b/directory/web/src/components/softwareCloud/imgs/34-Windows-command-line.png new file mode 100644 index 000000000..0741aa5b9 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/34-Windows-command-line.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/35-Shadowsocks.png b/directory/web/src/components/softwareCloud/imgs/35-Shadowsocks.png new file mode 100644 index 000000000..62cc490d4 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/35-Shadowsocks.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/36-ShadowsocksR.png b/directory/web/src/components/softwareCloud/imgs/36-ShadowsocksR.png new file mode 100644 index 000000000..9e0e2e76f Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/36-ShadowsocksR.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/37-Shadowrocket.png b/directory/web/src/components/softwareCloud/imgs/37-Shadowrocket.png new file mode 100644 index 000000000..816f0fd51 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/37-Shadowrocket.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/38-DNSRedirector.png b/directory/web/src/components/softwareCloud/imgs/38-DNSRedirector.png new file mode 100644 index 000000000..9742d8280 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/38-DNSRedirector.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/39-pfBlockerNG.png b/directory/web/src/components/softwareCloud/imgs/39-pfBlockerNG.png new file mode 100644 index 000000000..c831c6858 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/39-pfBlockerNG.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/40-Opera.png b/directory/web/src/components/softwareCloud/imgs/40-Opera.png new file mode 100644 index 000000000..359defd58 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/40-Opera.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/41-Surge.png b/directory/web/src/components/softwareCloud/imgs/41-Surge.png new file mode 100644 index 000000000..af42ee9d5 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/41-Surge.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/42-dnscrypt-proxy.png b/directory/web/src/components/softwareCloud/imgs/42-dnscrypt-proxy.png new file mode 100644 index 000000000..6acfbe273 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/42-dnscrypt-proxy.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/43-SmartDNS.png b/directory/web/src/components/softwareCloud/imgs/43-SmartDNS.png new file mode 100644 index 000000000..e5c398e72 Binary files /dev/null and b/directory/web/src/components/softwareCloud/imgs/43-SmartDNS.png differ diff --git a/directory/web/src/components/softwareCloud/imgs/imgs.d.ts b/directory/web/src/components/softwareCloud/imgs/imgs.d.ts new file mode 100644 index 000000000..a29b0ba00 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/imgs.d.ts @@ -0,0 +1,9 @@ +declare module "*.svg" { + const content: any; + export default content; +} + +declare module "*.png" { + const content: any; + export default content; +} diff --git a/directory/web/src/components/softwareCloud/imgs/index.ts b/directory/web/src/components/softwareCloud/imgs/index.ts new file mode 100644 index 000000000..fefbccf39 --- /dev/null +++ b/directory/web/src/components/softwareCloud/imgs/index.ts @@ -0,0 +1,87 @@ +import img01 from "./01-uBlock-Origin.svg"; +import img02 from "./02-Adblock-Plus.svg"; +import img03 from "./03-AdGuard.svg"; +import img04 from "./04-DNS66.png"; +import img05 from "./05-Nano-Adblocker.png"; +import img06 from "./06-AdBlock.png"; +import img07 from "./07-AdAway.png"; +import img08 from "./08-Personal-Blocklist.png"; +import img10 from "./10-Redirector.png"; +import img11 from "./11-Hosts-File-Editor.png"; +import img12 from "./12-Gas-Mask.png"; +import img13 from "./13-MinerBlock.svg"; +import img14 from "./14-Pi-hole.svg"; +import img15 from "./15-uBlock.svg"; +import img16 from "./16-Internet-Explorer-TPL.svg"; +import img17 from "./17-Google-Hit-Hider-by-Domain.png"; +import img18 from "./18-FireHOL.png"; +import img19 from "./19-Samsung-Knox.png"; +import img20 from "./20-Little-Snitch.png"; +import img21 from "./21-Privoxy.png"; +import img22 from "./22-Diversion.png"; +import img23 from "./23-dnsmasq.png"; +import img24 from "./24-Slimjet.png"; +import img25 from "./25-uMatrix.png"; +import img26 from "./26-Blokada.png"; +import img27 from "./27-hostsmgr.png"; +import img28 from "./28-personalDNSfilter.svg"; +import img29 from "./29-Unbound.png"; +import img30 from "./30-BIND.png"; +import img31 from "./31-AdGuard-Home.svg"; +import img32 from "./32-AdNauseam.png"; +import img33 from "./33-Legacy-Unix-Derivatives.png"; +import img34 from "./34-Windows-command-line.png"; +import img35 from "./35-Shadowsocks.png"; +import img36 from "./36-ShadowsocksR.png"; +import img37 from "./37-Shadowrocket.png"; +import img38 from "./38-DNSRedirector.png"; +import img39 from "./39-pfBlockerNG.png"; +import img40 from "./40-Opera.png"; +import img41 from "./41-Surge.png"; +import img42 from "./42-dnscrypt-proxy.png"; +import img43 from "./43-SmartDNS.png"; + +export { + img01, + img02, + img03, + img04, + img05, + img06, + img07, + img08, + img10, + img11, + img12, + img13, + img14, + img15, + img16, + img17, + img18, + img19, + img20, + img21, + img22, + img23, + img24, + img25, + img26, + img27, + img28, + img29, + img30, + img31, + img32, + img33, + img34, + img35, + img36, + img37, + img38, + img39, + img40, + img41, + img42, + img43 +}; diff --git a/directory/web/src/components/softwareCloud/index.ts b/directory/web/src/components/softwareCloud/index.ts new file mode 100644 index 000000000..6c9548df2 --- /dev/null +++ b/directory/web/src/components/softwareCloud/index.ts @@ -0,0 +1,4 @@ +import { SoftwareCloud } from "./SoftwareCloud"; +import { SoftwareIcon } from "./SoftwareIcon"; + +export { SoftwareCloud, SoftwareIcon }; diff --git a/directory/web/src/components/tagCloud/TagCloud.module.css b/directory/web/src/components/tagCloud/TagCloud.module.css new file mode 100644 index 000000000..879bc4070 --- /dev/null +++ b/directory/web/src/components/tagCloud/TagCloud.module.css @@ -0,0 +1,7 @@ +.grow { + /* allow for 2nd row's tags' bottom border to not be chopped in AllListsTable */ + max-height: none !important; + + /* leave vertical gap between rows of tags */ + line-height: 28px; +} diff --git a/directory/web/src/components/tagCloud/TagCloud.tsx b/directory/web/src/components/tagCloud/TagCloud.tsx new file mode 100644 index 000000000..ec36461fb --- /dev/null +++ b/directory/web/src/components/tagCloud/TagCloud.tsx @@ -0,0 +1,22 @@ +import { Tag } from "antd"; +import React from "react"; + +import { Tag as TagInterface } from "../../interfaces/Tag"; +import styles from "./TagCloud.module.css"; + +interface Props { + tags: TagInterface[]; + showLabel?: boolean; +} + +export const TagCloud = (props: Props) => + props.tags && props.tags.length ? ( +
+ {props.showLabel &&

{`Tag${props.tags.length > 1 ? "s" : ""}:`}

} + {props.tags.map((t: TagInterface, i: number) => ( + + {t.name} + + ))} +
+ ) : null; diff --git a/directory/web/src/components/tagCloud/index.ts b/directory/web/src/components/tagCloud/index.ts new file mode 100644 index 000000000..6cea6c0e9 --- /dev/null +++ b/directory/web/src/components/tagCloud/index.ts @@ -0,0 +1,3 @@ +import { TagCloud } from "./TagCloud"; + +export { TagCloud }; diff --git a/directory/web/src/constants.ts b/directory/web/src/constants.ts new file mode 100644 index 000000000..b8ecfa656 --- /dev/null +++ b/directory/web/src/constants.ts @@ -0,0 +1 @@ +export const SlugifyOptions = { remove: /[^a-zA-Z0-9- ]/g, lower: true }; diff --git a/directory/web/src/hooks/index.ts b/directory/web/src/hooks/index.ts new file mode 100644 index 000000000..56d07702d --- /dev/null +++ b/directory/web/src/hooks/index.ts @@ -0,0 +1,21 @@ +import { useLanguages } from "./useLanguages"; +import { useLicenses } from "./useLicenses"; +import { useLists } from "./useLists"; +import { useMaintainers } from "./useMaintainers"; +import { useSearchColumnFilter } from "./useSearchColumnFilter"; +import { useSoftware } from "./useSoftware"; +import { useSyntaxes } from "./useSyntaxes"; +import { useTablePageSizer } from "./useTablePageSizer"; +import { useTags } from "./useTags"; + +export { + useLanguages, + useLicenses, + useLists, + useMaintainers, + useSearchColumnFilter, + useSoftware, + useSyntaxes, + useTablePageSizer, + useTags, +}; diff --git a/directory/web/src/hooks/useApiData.tsx b/directory/web/src/hooks/useApiData.tsx new file mode 100644 index 000000000..3fdc03d53 --- /dev/null +++ b/directory/web/src/hooks/useApiData.tsx @@ -0,0 +1,11 @@ +import { useEffect, useState } from "react"; + +export const useApiData = (url: string) => { + const [data, setData] = useState(); + useEffect(() => { + const fetchData = async () => + (await fetch(url)).json().then((r) => setData(r)); + fetchData(); + }, [url]); + return data; +}; diff --git a/directory/web/src/hooks/useLanguages.tsx b/directory/web/src/hooks/useLanguages.tsx new file mode 100644 index 000000000..ece2dd0eb --- /dev/null +++ b/directory/web/src/hooks/useLanguages.tsx @@ -0,0 +1,7 @@ +import { Language } from "../interfaces/Language"; +import { useApiData } from "./useApiData"; + +export const useLanguages = () => + ( + useApiData("/api/v1/languages") || [] + ).sort((a: Language, b: Language) => a.name.localeCompare(b.name)); diff --git a/directory/web/src/hooks/useLicenses.tsx b/directory/web/src/hooks/useLicenses.tsx new file mode 100644 index 000000000..ccaeb9092 --- /dev/null +++ b/directory/web/src/hooks/useLicenses.tsx @@ -0,0 +1,7 @@ +import { License } from "../interfaces/License"; +import { useApiData } from "./useApiData"; + +export const useLicenses = () => + ( + useApiData("/api/v1/licenses") || [] + ).sort((a: License, b: License) => a.name.localeCompare(b.name)); diff --git a/directory/web/src/hooks/useLists.tsx b/directory/web/src/hooks/useLists.tsx new file mode 100644 index 000000000..bdaa0f5b7 --- /dev/null +++ b/directory/web/src/hooks/useLists.tsx @@ -0,0 +1,11 @@ +import slugify from "slugify"; + +import { SlugifyOptions } from "../constants"; +import { List } from "../interfaces/List"; +import { useApiData } from "./useApiData"; + +export const useLists = () => { + const lists = useApiData("/api/v1/lists"); + lists && lists.forEach((l) => (l.slug = slugify(l.name, SlugifyOptions))); + return (lists || []).sort((a: List, b: List) => a.name.localeCompare(b.name)); +}; diff --git a/directory/web/src/hooks/useMaintainers.tsx b/directory/web/src/hooks/useMaintainers.tsx new file mode 100644 index 000000000..66964df97 --- /dev/null +++ b/directory/web/src/hooks/useMaintainers.tsx @@ -0,0 +1,7 @@ +import { Maintainer } from "../interfaces/Maintainer"; +import { useApiData } from "./useApiData"; + +export const useMaintainers = () => + ( + useApiData("/api/v1/maintainers") || [] + ).sort((a: Maintainer, b: Maintainer) => a.name.localeCompare(b.name)); diff --git a/directory/web/src/hooks/useSearchColumnFilter.tsx b/directory/web/src/hooks/useSearchColumnFilter.tsx new file mode 100644 index 000000000..41872bd8b --- /dev/null +++ b/directory/web/src/hooks/useSearchColumnFilter.tsx @@ -0,0 +1,81 @@ +import { SearchOutlined } from "@ant-design/icons"; +import { Button, Input } from "antd"; +import React, { useEffect, useState } from "react"; +import { FilterDropdownProps } from "antd/lib/table/interface"; + +interface FilterPropsState { + filterDropdown?: + | React.ReactNode + | ((props: FilterDropdownProps) => React.ReactNode); + filterIcon?: React.ReactNode | ((filtered: boolean) => React.ReactNode); + onFilter?: (value: any, record: T) => boolean; +} + +export const useSearchColumnFilter = (dataIndex: string) => { + const [filterProps, setFilterProps] = useState>({ + filterDropdown: undefined, + filterIcon: undefined, + onFilter: undefined, + }); + useEffect(() => { + const handleSearch = (confirm?: () => void) => { + confirm && confirm(); + }; + const handleReset = (clearFilters?: (selectedKeys: string[]) => void) => { + clearFilters && clearFilters([]); + }; + setFilterProps({ + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + }) => ( +
+ + setSelectedKeys && + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => handleSearch(confirm)} + style={{ width: 188, marginBottom: 8, display: "block" }} + /> + + +
+ ), + filterIcon: (filtered) => ( + + ), + onFilter: (value, record) => { + const searchValue = (record as any)[dataIndex]; + return ( + searchValue && + searchValue + .toString() + .toLowerCase() + .includes(value.toString().toLowerCase()) + ); + }, + }); + }, [dataIndex]); + return filterProps; +}; diff --git a/directory/web/src/hooks/useSoftware.tsx b/directory/web/src/hooks/useSoftware.tsx new file mode 100644 index 000000000..2f33c0c3f --- /dev/null +++ b/directory/web/src/hooks/useSoftware.tsx @@ -0,0 +1,7 @@ +import { Software } from "../interfaces/Software"; +import { useApiData } from "./useApiData"; + +export const useSoftware = () => + ( + useApiData("/api/v1/software") || [] + ).sort((a: Software, b: Software) => a.name.localeCompare(b.name)); diff --git a/directory/web/src/hooks/useSyntaxes.tsx b/directory/web/src/hooks/useSyntaxes.tsx new file mode 100644 index 000000000..cf217ce06 --- /dev/null +++ b/directory/web/src/hooks/useSyntaxes.tsx @@ -0,0 +1,7 @@ +import { Syntax } from "../interfaces/Syntax"; +import { useApiData } from "./useApiData"; + +export const useSyntaxes = () => + ( + useApiData("/api/v1/syntaxes") || [] + ).sort((a: Syntax, b: Syntax) => a.name.localeCompare(b.name)); diff --git a/directory/web/src/hooks/useTablePageSizer.tsx b/directory/web/src/hooks/useTablePageSizer.tsx new file mode 100644 index 000000000..eee29d7d5 --- /dev/null +++ b/directory/web/src/hooks/useTablePageSizer.tsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +interface TablePageSize { + pageSize: number; + isNarrowWindow: boolean; + isWideWindow: boolean; +} + +export const useTablePageSizer = () => { + const [state, setState] = useState(calculateSize()); + useEffect(() => { + const updatePageSize = () => setState(calculateSize()); + window.addEventListener("resize", updatePageSize); + return () => window.removeEventListener("resize", updatePageSize); + }, []); + return state; +}; + +const calculateSize = () => ({ + pageSize: Math.floor((window.innerHeight - 184) / 57), + isNarrowWindow: window.innerWidth < 576 ? true : false, + isWideWindow: window.innerWidth > 1918 ? true : false, +}); diff --git a/directory/web/src/hooks/useTags.tsx b/directory/web/src/hooks/useTags.tsx new file mode 100644 index 000000000..6faf3cb9d --- /dev/null +++ b/directory/web/src/hooks/useTags.tsx @@ -0,0 +1,7 @@ +import { Tag } from "../interfaces/Tag"; +import { useApiData } from "./useApiData"; + +export const useTags = () => + (useApiData("/api/v1/tags") || []).sort((a: Tag, b: Tag) => + a.name.localeCompare(b.name) + ); diff --git a/directory/web/src/index.css b/directory/web/src/index.css new file mode 100644 index 000000000..4a1df4db7 --- /dev/null +++ b/directory/web/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/directory/web/src/index.tsx b/directory/web/src/index.tsx new file mode 100644 index 000000000..f0dbcb5c2 --- /dev/null +++ b/directory/web/src/index.tsx @@ -0,0 +1,8 @@ +import "./index.css"; + +import React from "react"; +import ReactDOM from "react-dom"; + +import { App } from "./App"; + +ReactDOM.render(, document.getElementById("root")); diff --git a/directory/web/src/interfaces/Language.ts b/directory/web/src/interfaces/Language.ts new file mode 100644 index 000000000..af4d69de7 --- /dev/null +++ b/directory/web/src/interfaces/Language.ts @@ -0,0 +1,6 @@ +export interface Language { + id: number; + filterListIds: number[]; + iso6391: string; + name: string; +} diff --git a/directory/web/src/interfaces/License.ts b/directory/web/src/interfaces/License.ts new file mode 100644 index 000000000..841df2905 --- /dev/null +++ b/directory/web/src/interfaces/License.ts @@ -0,0 +1,6 @@ +export interface License { + id: number; + descriptionUrl: string; + filterListIds: number[]; + name: string; +} diff --git a/directory/web/src/interfaces/List.ts b/directory/web/src/interfaces/List.ts new file mode 100644 index 000000000..901184ab1 --- /dev/null +++ b/directory/web/src/interfaces/List.ts @@ -0,0 +1,25 @@ +export interface List { + id: number; + chatUrl: string; + description: string; + descriptionSourceUrl: string; + donateUrl: string; + emailAddress: string; + forumUrl: string; + homeUrl: string; + issuesUrl: string; + languageIds: number[]; + licenseId: number; + maintainerIds: number[]; + name: string; + policyUrl: string; + publishedDate: string; + submissionUrl: string; + syntaxId: number; + tagIds: number[]; + viewUrl: string; + viewUrlMirrors: string[]; + + // auto-generated by useLists hook + slug: string; +} diff --git a/directory/web/src/interfaces/Maintainer.ts b/directory/web/src/interfaces/Maintainer.ts new file mode 100644 index 000000000..aa3746832 --- /dev/null +++ b/directory/web/src/interfaces/Maintainer.ts @@ -0,0 +1,8 @@ +export interface Maintainer { + id: number; + emailAddress: string; + filterListIds: number[]; + homeUrl: string; + name: string; + twitterHandle: string; +} diff --git a/directory/web/src/interfaces/Software.ts b/directory/web/src/interfaces/Software.ts new file mode 100644 index 000000000..dd8d151ac --- /dev/null +++ b/directory/web/src/interfaces/Software.ts @@ -0,0 +1,7 @@ +export interface Software { + id: number; + homeUrl: string; + isAbpSubscribable: boolean; + name: string; + syntaxIds: number[]; +} diff --git a/directory/web/src/interfaces/Syntax.ts b/directory/web/src/interfaces/Syntax.ts new file mode 100644 index 000000000..0873306d9 --- /dev/null +++ b/directory/web/src/interfaces/Syntax.ts @@ -0,0 +1,7 @@ +export interface Syntax { + id: number; + definitionUrl: string; + filterListIds: number[]; + name: string; + softwareIds: number[]; +} diff --git a/directory/web/src/interfaces/Tag.ts b/directory/web/src/interfaces/Tag.ts new file mode 100644 index 000000000..4141dd434 --- /dev/null +++ b/directory/web/src/interfaces/Tag.ts @@ -0,0 +1,6 @@ +export interface Tag { + id: number; + description: string; + filterListIds: number[]; + name: string; +} diff --git a/directory/web/src/react-app-env.d.ts b/directory/web/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/directory/web/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/directory/web/src/utils/index.ts b/directory/web/src/utils/index.ts new file mode 100644 index 000000000..844235139 --- /dev/null +++ b/directory/web/src/utils/index.ts @@ -0,0 +1,3 @@ +import { nameof } from "./nameof"; + +export { nameof }; diff --git a/directory/web/src/utils/nameof.ts b/directory/web/src/utils/nameof.ts new file mode 100644 index 000000000..282dd0ab2 --- /dev/null +++ b/directory/web/src/utils/nameof.ts @@ -0,0 +1,2 @@ +//https://stackoverflow.com/a/50470026/2343739 +export const nameof = (name: Extract) => name; diff --git a/directory/web/tsconfig.json b/directory/web/tsconfig.json new file mode 100644 index 000000000..af10394b4 --- /dev/null +++ b/directory/web/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src"] +} diff --git a/docker-compose.yml b/docker-compose.yml index 004110c24..4e36ba615 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,52 @@ version: "3.8" services: + filterlists.reverse_proxy: + image: nginx:alpine + networks: + - reverse_proxy + depends_on: + - filterlists.directory.web + - filterlists.directory.api + volumes: + - ./reverse-proxy/svr-includes:/etc/nginx/svr-includes:ro + + filterlists.directory.web: + image: ${DOCKER_REGISTRY-}filterlists_directory_web + build: + context: ./directory/web + networks: + - reverse_proxy + depends_on: + - filterlists.directory.api + filterlists.directory.api: image: ${DOCKER_REGISTRY-}filterlists_directory_api build: context: ./directory/api dockerfile: src/FilterLists.Directory.Api/Dockerfile + networks: + - reverse_proxy + - directory.api depends_on: - filterlists.directory.db + volumes: + - directory.db:/var/lib/postgresql/data environment: ConnectionStrings__DirectoryConnection: ${DIRECTORY_CONNECTION_STRING} filterlists.directory.db: image: postgres:alpine - restart: always + networks: + - directory.api environment: POSTGRES_USER: ${DIRECTORY_DB_USER} POSTGRES_PASSWORD: ${DIRECTORY_DB_PASSWORD} - POSTGRES_DB: ${DIRECTORY_DB_DATABASE} \ No newline at end of file + POSTGRES_DB: ${DIRECTORY_DB_DATABASE} + +networks: + reverse_proxy: + directory.api: + +volumes: + directory.db: \ No newline at end of file diff --git a/reverse-proxy/conf.d.dev/filterlists.conf b/reverse-proxy/conf.d.dev/filterlists.conf new file mode 100644 index 000000000..ab9c4f6cc --- /dev/null +++ b/reverse-proxy/conf.d.dev/filterlists.conf @@ -0,0 +1,26 @@ +server { + listen 80 default_server; + + location ^~ /api { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://api:80; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://web:80; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/reverse-proxy/conf.d/filterlists.conf b/reverse-proxy/conf.d/filterlists.conf new file mode 100644 index 000000000..758764ac4 --- /dev/null +++ b/reverse-proxy/conf.d/filterlists.conf @@ -0,0 +1,54 @@ +server { + listen 443 default_server ssl http2; + server_name filterlists.com; + + include svr-includes/*; + + ssl_certificate /etc/nginx/cert/filterlists_cert.pem; + ssl_certificate_key /etc/nginx/cert/filterlists_key.pem; + + add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; manifest-src 'self';" always; + + location ^~ /api { + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://api:80; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://web:80; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# Redirect http to https +server { + listen 80 default_server; + server_name filterlists.com; + + return 301 https://filterlists.com$request_uri; +} + +# Redirect www to non-www +server { + listen 80; + listen 443 ssl http2; + server_name www.filterlists.com; + + ssl_certificate /etc/nginx/cert/filterlists_cert.pem; + ssl_certificate_key /etc/nginx/cert/filterlists_key.pem; + + return 301 https://filterlists.com$request_uri; +} \ No newline at end of file diff --git a/reverse-proxy/conf.d/static-files.conf b/reverse-proxy/conf.d/static-files.conf new file mode 100644 index 000000000..cad06a7a7 --- /dev/null +++ b/reverse-proxy/conf.d/static-files.conf @@ -0,0 +1,77 @@ +# https://raw.githubusercontent.com/h5bp/server-configs-nginx/master/h5bp/web_performance/cache_expiration.conf +# ---------------------------------------------------------------------- +# | Cache expiration | +# ---------------------------------------------------------------------- + +# Serve resources with far-future expiration date. +# +# (!) If you don't control versioning with filename-based cache busting, you +# should consider lowering the cache times to something like one week. +# +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires +# https://nginx.org/en/docs/http/ngx_http_headers_module.html#expires + +map $sent_http_content_type $expires { + default 1M; + + # No content + "" off; + + # CSS + ~*text/css 1y; + + # Data interchange + ~*application/atom\+xml 1h; + ~*application/rdf\+xml 1h; + ~*application/rss\+xml 1h; + + ~*application/json 4h; + ~*application/ld\+json 0; + ~*application/schema\+json 0; + ~*application/geo\+json 0; + ~*application/xml 0; + ~*text/calendar 0; + ~*text/xml 0; + + # Favicon (cannot be renamed!) and cursor images + ~*image/vnd.microsoft.icon 1w; + ~*image/x-icon 1w; + + # HTML + ~*text/html 0; + + # JavaScript + ~*application/javascript 1y; + ~*application/x-javascript 1y; + ~*text/javascript 1y; + + # Manifest files + ~*application/manifest\+json 1w; + ~*application/x-web-app-manifest\+json 0; + ~*text/cache-manifest 0; + + # Markdown + ~*text/markdown 0; + + # Media files + ~*audio/ 1M; + ~*image/ 1M; + ~*video/ 1M; + + # WebAssembly + ~*application/wasm 1y; + + # Web fonts + ~*font/ 1M; + ~*application/vnd.ms-fontobject 1M; + ~*application/x-font-ttf 1M; + ~*application/x-font-woff 1M; + ~*application/font-woff 1M; + ~*application/font-woff2 1M; + + # Other + ~*text/x-cross-domain-policy 1w; +} + +expires $expires; diff --git a/reverse-proxy/svr-includes/security.conf b/reverse-proxy/svr-includes/security.conf new file mode 100644 index 000000000..a90571540 --- /dev/null +++ b/reverse-proxy/svr-includes/security.conf @@ -0,0 +1,6 @@ +server_tokens off; + +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-XSS-Protection "1; mode=block" always; +add_header Referrer-Policy "no-referrer" always; \ No newline at end of file diff --git a/reverse-proxy/svr-includes/ssl.conf b/reverse-proxy/svr-includes/ssl.conf new file mode 100644 index 000000000..e36eacd41 --- /dev/null +++ b/reverse-proxy/svr-includes/ssl.conf @@ -0,0 +1,18 @@ +# https://ssl-config.mozilla.org/#server=nginx&config=intermediate&ocsp=false + +ssl_session_timeout 1d; +ssl_session_cache shared:MozSSL:10m; # about 40000 sessions +ssl_session_tickets off; + +ssl_dhparam /etc/nginx/cert/dhparam.pem; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_prefer_server_ciphers off; + +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + +# https://support.cloudflare.com/hc/en-us/articles/204899617-Authenticated-Origin-Pulls + +ssl_client_certificate /etc/nginx/cert/cloudflare.crt; +ssl_verify_client on;