Merge branch 'EbookFoundation:main' into main

pull/24/head
LeoOuyang24 2022-05-06 12:47:07 -07:00 committed by GitHub
commit 806a9d6d46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 61117 additions and 27648 deletions

49
.github/workflows/parser.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: free-programming-books-parse
on:
workflow_dispatch:
inputs:
reason:
required: true
default: "update json files"
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout self
uses: actions/checkout@v2
with:
path: json
- name: Checkout programming books
uses: actions/checkout@v2
with:
repository: EbookFoundation/free-programming-books
path: fpb
- name: Checkout parser
uses: actions/checkout@v2
with:
repository: EbookFoundation/free-programming-books-parser
path: parser
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
- run: npm install -g https://github.com/EbookFoundation/free-programming-books-parser
- run: fpb-parse --output ./json/fpb.json
- name: Commit Changes
run: |
cd './json'
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add -f 'fpb.json'
git commit -m "update fpb.json"
- name: Push changes
uses: ad-m/github-push-action@master
with:
directory: "./json"

View File

@ -1,70 +1,61 @@
# Free Programming Books Search
# free-programming-books-search
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
The free-programming-books-search is a companion project of [free-programming-books](https://ebookfoundation.github.io/free-programming-books/). It allows users to search by book title or author and filter by language. The search index is updated once per day, so changes made on [free-programming-books](https://ebookfoundation.github.io/free-programming-books/) may not be immediately reflected.
## Available Scripts
## Contents
In the project directory, you can run:
- [Contents](#contents)
- [Installation](#installation)
- [NPM Installation](#npm-installation)
- [Running the Website](#running-the-website)
- [Deployment](#deployment)
- [How It All Works](#how-it-all-works)
- [FAQ](#faq)
### `npm start`
## Installation
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
### NPM Installation
The page will reload if you make edits.\
You will also see any lint errors in the console.
1. Make sure you have [Node.js](https://nodejs.org/en/) installed. If you already do, skip to [Running the Website](#running-the-website).
2. Otherwise, download the LTS installer from [Node.js](https://nodejs.org/en/) website.
3. Follow the instructions of the installer, make sure npm is listed as a package to be installed.
4. Click Install.
5. Verify that Node.Js has been installed by going to command line and typing in `node`. It should show the current version.
6. Close out of Node by either closing and reopening the command line or with <kbd>Ctrl + C</kbd>.
7. Make sure to check out the [NPM website](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) for more info.
### `npm test`
### Running the Website
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
1. Make sure you have [Git](https://git-scm.com/downloads) installed.
2. Clone the repo from Github with Git.
3. Navigate to the folder using command line. A easy way is to type "`cd`" and then drag and drop the folder into command line.
4. Type `npm install`.
5. Type `npm install react-scripts`.
6. Type `npm start`. At this point, the command prompt should start up the server, and a tab in your default browser should open up to localhost.
### `npm run build`
## Deployment
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
MAKE SURE YOU HAVE COMPLETED THE INSTALLATION STEPS FIRST!
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
1. First, make sure that you the local folder containing the files has a remote configured called "`origin`".
1. If you aren't sure, navigate to the folder using Git (type "`cd`", then drag and drop folder in to Git command line).
2. Type `git init`.
3. Type `git remote add origin <repo url>`, replacing `<repo url>` with the url of your github repository.
2. Now, run `npm install -g gh-pages`.
3. Run `npm run deploy`.
4. This should deploy your code to "`https://yourusername.github.io/free-programming-books-search/`".
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
## How It All Works
### `npm run eject`
1. THERE IS NO DATABASE INVOLVED. Rather, the books are stored in a markdown on [
free-programming-books](https://ebookfoundation.github.io/free-programming-books/) and is parsed daily by [free-programming-books-parser](https://github.com/EbookFoundation/free-programming-books-parser). The books and all info pertaining to them are stored in a JSON file called `fpb.json`.
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
2. This JSON is downloaded locally and searched locally when the actual search function is used.
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
## FAQ
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
- What database are we using to store the books?
- NONE! The books are stored in a JSON file which is downloaded locally.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
- I added a book but it's not showing up on search?
- Give it some time. The parser is run once a day, so it may take up to 24 hours for the search to reflect that.

52948
fpb.json Normal file

File diff suppressed because it is too large Load Diff

13469
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,16 @@
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.24.0",
"express": "^4.17.1",
"fuse": "^0.4.0",
"fuse.js": "^6.4.6",
"jQuery": "^1.7.4",
"query-string": "^7.1.1",
"react": "^17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-markdown": "^8.0.3",
"react-scripts": "4.0.3",
"rehype-raw": "^6.1.1",
"rehype-slug": "^5.0.1",
"web-vitals": "^1.1.2"
},
"scripts": {

View File

@ -22,7 +22,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>FPB Search</title>
<title>free-programming-books | Freely available programming books</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,4 +1,301 @@
body {
background-color: #fff;
padding:50px;
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
color:#595959;
font-weight:400;
}
h1, h2, h3, h4, h5, h6 {
color:#222;
margin:0 0 20px;
}
p, ul, ol, table, pre, dl {
margin:0 0 20px;
}
h1, h2, h3 {
line-height:1.1;
}
h1 {
font-size:28px;
font-weight: bold;
}
h2 {
color:#393939;
font-weight: bold;
}
h3, h4, h5, h6 {
color:#494949;
font-weight: bold;
}
a {
color:#267CB9;
text-decoration:none;
}
a:hover {
color:#069;
font-weight: bold;
}
a small {
font-size:11px;
color:#777;
margin-top:-0.3em;
display:block;
}
a:hover small {
color:#777;
}
.wrapper {
width:860px;
margin:0 auto;
}
blockquote {
border-left:1px solid #e5e5e5;
margin:0;
padding:0 0 0 20px;
font-style:italic;
}
code, pre {
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
color:#333;
}
pre {
padding:8px 15px;
background: #f8f8f8;
border-radius:5px;
border:1px solid #e5e5e5;
overflow-x: auto;
}
table {
width:100%;
border-collapse:collapse;
}
th, td {
text-align:left;
padding:5px 10px;
border-bottom:1px solid #e5e5e5;
}
dt {
color:#444;
font-weight:500;
}
th {
color:#444;
}
img {
max-width:100%;
}
header {
width:270px;
float:left;
position:fixed;
-webkit-font-smoothing:subpixel-antialiased;
}
header ul {
list-style:none;
height:40px;
padding:0;
background: #f4f4f4;
border-radius:5px;
border:1px solid #e0e0e0;
width:270px;
}
header li {
width:89px;
float:left;
border-right:1px solid #e0e0e0;
height:40px;
}
header li:first-child a {
border-radius:5px 0 0 5px;
}
header li:last-child a {
border-radius:0 5px 5px 0;
}
header ul a {
line-height:1;
font-size:11px;
color:#999;
display:block;
text-align:center;
padding-top:6px;
height:34px;
}
header ul a:hover {
color:#999;
}
header ul a:active {
background-color:#f0f0f0;
}
strong {
color:#222;
font-weight:500;
}
header ul li + li + li {
border-right:none;
width:89px;
}
header ul a strong {
font-size:14px;
display:block;
color:#222;
}
section {
width:500px;
float:right;
padding-bottom:50px;
}
small {
font-size:11px;
}
hr {
border:0;
background:#e5e5e5;
height:1px;
margin:0 0 20px;
}
footer {
width:270px;
float:left;
position:fixed;
bottom:50px;
-webkit-font-smoothing:subpixel-antialiased;
}
@media print, screen and (max-width: 960px) {
div.wrapper {
width:auto;
margin:0;
}
header, section, footer {
float:none;
position:static;
width:auto;
}
header {
padding-right:320px;
}
section {
border:1px solid #e5e5e5;
border-width:1px 0;
padding:20px 0;
margin:0 0 20px;
}
header a small {
display:inline;
}
header ul {
position:absolute;
right:50px;
top:52px;
}
}
@media print, screen and (max-width: 720px) {
body {
word-wrap:break-word;
}
header {
padding:0;
}
header ul, header p.view {
position:static;
}
pre, code {
word-wrap:normal;
}
}
@media print, screen and (max-width: 480px) {
body {
padding:15px;
}
header ul {
width:99%;
}
header li, header ul li + li + li {
width:33%;
}
}
@media print {
body {
padding:0.4in;
font-size:12pt;
color:#444;
}
}
form {
visibility: hidden;
}
.searchbar {
padding-bottom: 0.3em;
visibility: visible;
}
.searchterm {
border: 1px solid #666;
border-radius: 0.3em;
padding: 0.15em 0.15em 0.15em 0.15em;
width: 15em;
}
.languages {
border: 1px solid #666;
border-radius: 0.3em;
padding: 0.15em 0.15em 0.15em 0.15em;
width: 15.4em;
visibility: visible;
}
.sect-drop {
box-sizing: border-box;
background-color: #222222;
color: white;
@ -8,11 +305,26 @@ body {
box-sizing: border-box;
}
.white-content {
background-color: white;
color: black;
.dark-content {
background-color: black;
color: #D4CECD
}
.dark-content h1, .dark-content h2, .dark-content h3, .dark-content h4, .dark-content h5, .dark-content h6 {
color: #DDDDDD !important;
}
.dark-content small{
color: #A29D9C
}
.dark-content a{
color: #58a0d3;
}
.frontPage {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
@ -26,10 +338,6 @@ body {
justify-content: center;
}
a{
color: inherit;
}
.search-results {
justify-content: center;
align-items: flex-start;
@ -44,6 +352,31 @@ a{
}
.result {
width: 30%;
/* width: 30%; */
padding: 0.25em 0.5em;
}
.filters {
margin-top: -1em;
max-height: 13em;
overflow: scroll;
width: 18em;
overflow-x: hidden;
visibility: visible;
}
.filterHeader {
display: flex;
justify-content: left;
}
.filterHeader button {
margin-left: 1em;
width: 1.5em;
height: 1.7em;
}
.langFilters {
margin-top: 0.5em;
}

View File

@ -1,80 +1,31 @@
import React, { useState, useEffect, createContext } from "react";
import LangDropdown from "./components/LangDropdown";
import SearchBar from "./components/SearchBar";
import SearchResult from "./components/SearchResult";
import LightSwitch from "./components/LightSwitch";
import React, { useState, useEffect } from "react";
import axios from "axios";
import Fuse from "fuse.js";
import { ThemeContext, themes, swapMode } from './darkMode';
import { useCookies } from 'react-cookie';
import queryString from "query-string";
import SunImg from "./img/sun.png"
import MoonImg from "./img/moon.png"
import LangFilters from "./components/LangFilters";
import SearchBar from "./components/SearchBar";
import SearchResult from "./components/SearchResult";
import MarkdownParser from "./components/MarkdownParser";
const fpb = null;
// eslint-disable-next-line
function makeBook(author, hLang, cLang, title, url) {
//returns a struct with basic book info (author, human language, computer language, book title, url)
return {
author: author,
hLang: hLang, //human language
cLang: cLang, //computer language
title: title,
url: url,
};
}
// eslint-disable-next-line
function forEachBook(func, json) {
//Runs func on each section, entry, and book in json, which is a list of entries
if (typeof func !== "function") {
// eslint-disable-next-line
throw "ERROR in forEachBook: parameter not a fucntion";
}
for (const hLang in json) {
//for each human language
if (Array.isArray(hLang.sections)) {
//check if sections is an array
hLang.sections.forEach(
(
cLang //for each computer lanuage
) => {
if (Array.isArray(cLang.entries)) {
//verify is entries is an array
cLang.entries.forEach(
(
book //for each book
) => {
if (typeof book === "object") {
//verify that book is an object
func(json[hLang], cLang, book); //run the function
}
}
);
}
}
);
}
}
}
// Sorts search results by their score
// eslint-disable-next-line
function sortByScore(results) {
results.sort(function (a, b) {
return a.score - b.score;
});
return results;
}
import SunImg from "./img/sun.png";
import MoonImg from "./img/moon.png";
import { ThemeContext, themes, swapMode } from "./darkMode";
function jsonToArray(json) {
// list of all books
let arr = [];
// list of all topics (sections)
let sections = [];
json.children[0].children.forEach((document) => {
// for each markdown document
for (let i = 0; i < json.children.length; i++) {
json.children[i].children.forEach((document) => {
// for each topic in the markdown
// these are typically h2 and h3 tags in the markdown
document.sections.forEach((section) => {
// Add section to master list if it's not there
if (!sections.includes(section.section)) sections.push(section.section);
// Add new entries that were under an h2 tag
section.entries.forEach((entry) => {
arr.push({
author: entry.author,
@ -84,6 +35,7 @@ function jsonToArray(json) {
section: section.section,
});
});
// Add new entries that were under an h3 tag
section.subsections.forEach((subsection) => {
subsection.entries.forEach((entry) => {
arr.push({
@ -98,50 +50,59 @@ function jsonToArray(json) {
});
});
});
}
return { arr: arr, sections: sections };
}
function App() {
const [data, setData] = useState(undefined); // keeps the state of the json
const [dataArray, setDataArray] = useState([]); // put everything into one array. uses more memory, but search is faster and less complex
// eslint-disable-next-line
const [index, setIndex] = useState([]); // used for "table of contents". currently unused
const [loading, setLoading] = useState(true); // Determines whether to show spinner
const [searchParams, setSearchParams] = useState({ title: "" });
// keeps the state of the json
const [data, setData] = useState(undefined);
// put all books into one array. uses more memory, but search is faster and less complex
const [dataArray, setDataArray] = useState([]);
// Keeps track if all resources are loaded
const [loading, setLoading] = useState(true);
// State keeping track of all search parameters
// use the changeParameter function to set, NOT setSearchParams
// changeParameter will retain the rest of the state
let defaultSearch = queryString.parse(document.location.search).search || "";
const [searchParams, setSearchParams] = useState({ searchTerm: defaultSearch, "lang.code": "" });
// array of all search results
const [searchResults, setSearchResults] = useState([]);
const [sectionResults, setSectionResults] = useState([]);
const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
const [queries, setQueries] = useState({ lang: "", subject: "" });
// eslint-disable-next-line
const [error, setError] = useState("");
let resultsList = null; // the html string containing the search results
let sectionResultsList = null;
// Used to change the search parameters state
// Heavily used in child components to set the state
const changeParameter = (param, value) => {
// Lets a child component set the value of the search term
setSearchParams({ ...searchParams, [param]: value });
};
// fetches data the first time the page renders
useEffect(() => {
swapMode(cookies.lightMode ? themes.lightMode : themes.darkMode)
async function fetchData() {
try {
setLoading(true);
setQueries(queryString.parse(document.location.search));
if (queries.lang) {
if (queries.lang === "langs" || queries.lang === "subjects") {
changeParameter("lang.code", "en");
} else {
changeParameter("lang.code", queries.lang);
}
}
// setLoading(true);
let result = await axios.get(
"https://raw.githubusercontent.com/FreeEbookFoundationBot/free-programming-books-json/main/fpb.json"
"https://raw.githubusercontent.com/EbookFoundation/free-programming-books-search/main/fpb.json"
);
setData(result.data);
// eslint-disable-next-line
let { arr, sections } = jsonToArray(result.data);
setDataArray(arr);
setIndex(sections);
} catch (e) {
// setError("Couldn't get data. Please try again later")
setData(fpb);
let { arr, sections } = jsonToArray(fpb);
setIndex(sections);
setDataArray(arr);
setError("Couldn't get data. Please try again later")
}
setLoading(false);
}
@ -149,97 +110,221 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
}, []);
// fires when searchTerm changes
// THIS IS THE MAIN SEARCH FUNCTION CURRENTLY
// Finds most relevant title or author
// THIS IS THE MAIN SEARCH FUNCTION
useEffect(() => {
if (dataArray) {
// Finds most relevant titles
const fuseOptions = {
useExtendedSearch: true,
findAllMatches: true,
shouldSort: true,
includeScore: true,
threshold: 0.2,
keys: ["title", "lang.code"],
useExtendedSearch: true, // see fuse.js documentation
findAllMatches: true, //continue searching after first match
shouldSort: true, // sort by proximity score
includeScore: true, // includes score in results
includeMatches: true,
threshold: 0.2, // threshold for fuzzy-search,
keys: ["author", "title", "lang.code", "section"],
};
// create new fuse given the array of books and the fuse options from above
let fuse = new Fuse(dataArray, fuseOptions);
let query = [];
let andQuery = []; // for filters that MUST be matched, like language
let orQuery = []; // filters where any may be matched, like author or title
// for each search param
for (const [key, value] of Object.entries(searchParams)) {
if (value === null || value === "") continue;
if (key === "lang.code") {
query.push({ "lang.code": `^${value}` });
continue;
if (key === "lang.code" || key === "section") {
// the '^' means it must be an exact match at the beginning
// this is because lang.code and section are strict filters
andQuery.push({ [key]: `^${value}` });
}
query.push({ [key]: value });
if (key === "searchTerm") {
orQuery.push({ author: value });
orQuery.push({ title: value });
}
}
// Nest the 'or' query inside the 'and' query
// Necessary step, a quirk with fuse.js
andQuery.push({ $or: orQuery });
// Perform the search
let result = fuse.search({
$and: query,
$and: andQuery,
});
// filter to top results
result = result.slice(0, 40);
setSearchResults(result);
// console.log(result)
let sResults = []; // section results
// Finds the most relevant sections
result.forEach((entry) => {
let section = entry.item.section;
if (!sResults.includes(section)) sResults.push(section);
});
setSectionResults(sResults);
// Goes through the list of results
// let relevantLists = [];
// result.forEach((entry) => {
// // Checks if a new entry has already been made with the given programming language and human language.
// let obj = relevantLists.find(
// (o) => o.item.section === entry.item.section && o.item.lang.code === entry.item.lang.code
// );
// if (!obj && entry.item.lang.code) {
// let langCode = entry.item.lang.code;
// let section = entry.item.subsection ? entry.item.subsection : entry.item.section;
// // English is split into the subjects and langs file. The parser flags which type of entry it is to use here
// if (langCode === "en") {
// if (entry.item.lang.isSubject) {
// langCode = "subjects";
// } else {
// langCode = "langs";
// }
// }
// // Consider moving function out of here
// let id = section;
// // Some ids are in HTML tags, so this will extract that id to form proper links
// if (id.includes("<a")) {
// let x = id.match(/"(.*?)"/)[0];
// id = x ? x.replaceAll(/\"/g, "") : "FAIL";
// section = id;
// }
// // List of id properties fixed with this line:
// // 1. Must be all lowercase
// // 2. Spaces are hyphens
// // 3. Parentheses, ampersands, and slashes aren't allowed at all
// id = id
// .toLowerCase()
// .replaceAll(" ", "-")
// .replaceAll(/\(|\)|\&|\/|\./g, "");
// // Creates a listing for the broader list of entries
// let listing = {
// item: {
// author: "",
// lang: entry.item.lang,
// section: entry.item.section,
// title: `List of all ${section} books in ${entry.item.lang.name}`,
// url: `/free-programming-books-search?sect=books&lang=${langCode}&file=free-programming-books-${langCode}#${section}`,
// samePage: true,
// },
// };
// relevantLists.push(listing);
// }
// });
// // Keep only the first 5 as more than that became cumbersome with broad searches
// relevantLists = relevantLists.slice(0, 5);
// result = relevantLists.concat(result);
setSearchResults(result);
// console.log(result);
}
}, [searchParams, dataArray]);
if (loading) {
// if still fetching resource
return <h1>Loading...</h1>;
}
// if (loading) {
// // if still fetching resource
// return <h1>Loading...</h1>;
// }
if (error) {
return <h1>Error: {error}</h1>;
}
if (searchParams.title && searchResults.length !== 0) {
if (searchParams.searchTerm && searchResults.length !== 0) {
resultsList =
searchResults &&
searchResults.map((entry) => {
return <SearchResult data={entry.item} />;
});
sectionResultsList =
sectionResults &&
sectionResults.map((section) => {
return <li>{section}</li>;
});
}
return (
<div className="frontPage" >
<div className="wrapper">
<ThemeContext.Consumer>
{({ changeTheme }) => {
let willBeDarkMode = (cookies.lightMode && cookies.lightMode.toLowerCase() !== "true") //whether or not we are currently light mode and will become dark mode
changeTheme(willBeDarkMode ? themes.light : themes.dark)
return (<img src={willBeDarkMode ? SunImg: MoonImg}
let willBeDarkMode = (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches); //whether or not we are currently light mode and will become dark mode
changeTheme(willBeDarkMode ? themes.light : themes.dark);
return (
<img
alt="Toggle light/dark mode"
src={willBeDarkMode ? MoonImg : SunImg}
onClick={() => {
setCookie("lightMode", willBeDarkMode);
changeTheme(willBeDarkMode ? themes.light : themes.dark)
changeTheme(willBeDarkMode ? themes.light : themes.dark);
}}
style={{width: "20px", height: "20px",display: "block",
marginLeft: "auto"
style={{ width: "20px", height: "20px", display: "block", marginLeft: "auto" }}
/>
);
}}
/>)}
}
</ThemeContext.Consumer>
<header className="header">
<h1>
<a href="/free-programming-books-search/">free-programming-books</a>
</h1>
<h1>Free Programming Books</h1>
<div>
<SearchBar changeParameter={changeParameter} />
<LangDropdown changeParameter={changeParameter} data={data} />
</div>
<h2>Section Results</h2>
{sectionResultsList && (
<p>
This feature is not complete! For now, use this to help reference the
markdown documents on the main respository.
<img
className="emoji"
title=":books:"
alt=":books:"
src="https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png"
height="20"
width="20"
/>{" "}
Freely available programming books
</p>
<p className="view">
<a href="https://github.com/EbookFoundation/free-programming-books" target="_blank" rel="noreferrer">
View the Project on GitHub <small>EbookFoundation/free-programming-books</small>
</a>
</p>
<p>
Does a link not work?
<br />
<a href="https://github.com/EbookFoundation/free-programming-books/issues/" target="_blank" rel="noreferrer">
Report an error on GitHub
</a>
</p>
<div>
{loading ? (
<p />
) : (
<div>
<SearchBar changeParameter={changeParameter} defaultTerm={searchParams.searchTerm} />{" "}
<LangFilters changeParameter={changeParameter} data={data} langCode={searchParams["lang.code"]} />{" "}
</div>
)}
<div className="search-results">{sectionResultsList}</div>
<h2>Top Results</h2>
<div className="search-results">{resultsList}</div>
</div>
</header>
<section className="search-results">
{loading ? (
<p>Loading</p>
) : resultsList ? (
<div>
<br />
<h2>Search Results</h2>
<ul>{resultsList}</ul>
</div>
) : searchParams.searchTerm ? (
<div>
<br />
<h2>No results found.</h2>
</div>
) :
<MarkdownParser file={queries.file} sect={queries.sect} />
}
</section>
<footer>
<p>
This project is maintained by{" "}
<a href="https://github.com/EbookFoundation" target="_blank" rel="noreferrer">
EbookFoundation
</a>
</p>
<p>
<small>
Hosted on GitHub Pages Theme by{" "}
<a href="https://github.com/orderedlist" target="_blank" rel="noreferrer">
orderedlist
</a>
</small>
</p>
</footer>
</div>
);
}

View File

@ -1,57 +0,0 @@
import React, { useState, useEffect } from "react";
function LangDropdown({ changeParameter, data }) {
const [languages, setLanguages] = useState([]);
let options = null;
const handleChange = (e) => {
changeParameter("lang.code", e.target.value);
};
useEffect(
// run whenever data changes
() => {
if (data) {
let langArray = [];
data.children[0].children.forEach((document) => {
if (
typeof document.language.name === "string" &&
document.language.name.length > 0
) {
//make sure the language is valid and not blank
//console.log("LANGUAGE: " + document.language.name)
langArray.push(document.language);
}
});
langArray.sort((a, b) => a.name > b.name);
setLanguages(langArray);
}
},
[data]
);
const createOption = (language) => {
return (
<option class="lang" key={language.code} value={language.code}>
{language.name}
</option>
);
};
options =
languages &&
languages.map((language) => {
return createOption(language);
});
// console.log(options);
return (
<select onChange={handleChange} name="languages" id="languages">
<option key="allLangs" value="">
All Languages
</option>
{options}
</select>
);
}
export default LangDropdown;

View File

@ -0,0 +1,105 @@
import { React, useState, useEffect } from "react";
import queryString from "query-string";
function LangFilters({ changeParameter, data, langCode }) {
const [languages, setLanguages] = useState([]);
const [selected, setSelected] = useState(langCode);
const [showFilters, setShow] = useState(false);
let options = null;
const handleChange = (e) => {
changeParameter("lang.code", e.target.value);
setSelected(e.target.value);
};
useEffect(() => {
let queries = queryString.parse(document.location.search);
if (queries.lang) {
if (queries.lang === "langs" || queries.lang === "subjects") {
changeParameter("lang.code", "en");
setSelected("en");
} else {
changeParameter("lang.code", queries.lang);
setSelected(queries.lang);
}
} else {
changeParameter("lang.code", "");
setSelected("")
}
}, []);
useEffect(
// run whenever data changes
() => {
if (data) {
let langArray = [{ code: "en", name: "English" }];
data.children[0].children.forEach((document) => {
if (typeof document.language.name === "string" && document.language.name.length > 0) {
//make sure the language is valid and not blank
//console.log("LANGUAGE: " + document.language.name)
if (document.language.code !== "en") {
// used to ensure only one English is listed
langArray.push(document.language);
}
}
});
langArray.sort((a, b) => a.name > b.name);
setLanguages(langArray);
}
},
[data]
);
const createOption = (language) => {
return (
<div>
<label>
<input
type="radio"
className="lang"
key={language.code}
value={language.code}
onChange={handleChange}
checked={language.code === selected}
/>
{language.name}
</label>
</div>
);
};
options =
languages &&
languages.map((language) => {
return createOption(language);
});
let filterList = (
<form className="filters">
<label>
<input
type="radio"
key="all"
className="sect-select"
value=""
onChange={handleChange}
checked={"" === selected}
/>
All Languages
</label>
{options}
</form>
);
return (
<div className="langFilters">
<div className="filterHeader">
<h3>Filter by Language</h3>
<button onClick={() => setShow(!showFilters)}>{showFilters ? "-" : "+"}</button>
</div>
{showFilters ? filterList : ""}
</div>
);
}
export default LangFilters;

View File

@ -0,0 +1,79 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
import ReactMarkdown from "react-markdown";
import rehypeSlug from "rehype-slug";
import rehypeRaw from "rehype-raw";
import ParsedLink from "./ParsedLink";
function MarkdownParser({ file, sect }) {
let [markdown, setMarkdown] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
// console.log({sect: sect, file: file});
setLoading(true);
let result = null;
if (sect && file) {
// Both sect and file exist so construct the URL with both parameters
result = await axios.get(
`https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/${sect}/${file}`
);
} else if (!sect && file) {
// Occurs when getting a file from the root directory
result = await axios.get(
`https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/${file}`
);
} else {
// Default to getting the README
result = await axios.get(
`https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/README.md`
);
}
setMarkdown(result.data);
} catch (e) {
console.log("Couldn't get data. Please try again later");
}
setLoading(false);
}
fetchData();
}, [file, sect]);
if (loading) {
return <p>Loading...</p>;
}
if (!markdown) {
return <p>Error: Could not retrieve data.</p>;
}
return (
<section>
<ReactMarkdown
children={markdown}
remarkRehypeOptions={{ allowDangerousHtml: true }} // HTML is required for the all ids to be targetable
rehypePlugins={[rehypeSlug, rehypeRaw]}
components={{
// Replaces relative links in a markdown file with a parsed version of the link
// All other links are left untouched
a({ className, children, href, id}) {
if (href.startsWith("http") || href.charAt(0) === "#") {
return (
<a className={className} href={href} id={id}>
{children}
</a>
);
}
return <ParsedLink children={children} className={className} sect={sect} href={href} id={id}/>;
},
}}
/>
</section>
);
}
export default MarkdownParser;

View File

@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
function ParsedLink({ children, sect, href, id }) {
const [folder, setFolder] = useState(null);
const [file, setFile] = useState(null);
useEffect(() => {
// Splits the original link into the folder and file names
// If there is only one entry then the folder is the root directory and the entry is the file
let hrefSplit = href.split("/");
if (hrefSplit.length === 2) {
// Some docs reference back to the root directory which would give the folder ".."
// When that happens, skip setting the folder as it should stay null.
if (hrefSplit[0] !== "..") {
setFolder(hrefSplit[0]);
}
setFile(hrefSplit[1]);
} else {
// Only a file is given
setFile(hrefSplit[0]);
// When the current section is docs, all relative links stay in docs
if (sect === "docs") {
setFolder(sect);
} else {
setFolder(null);
}
}
}, [href]);
if (folder && file) {
return <a id={id} href={`/free-programming-books-search/?&sect=${folder}&file=${file}`}>{children}</a>;
} else if (file) {
return <a id={id} href={`/free-programming-books-search/?file=${file}`}>{children}</a>;
} else { // Go to the homepage when there's a bad relative URL
return <a id={id} href={`/free-programming-books-search/`}>{children}</a>
}
}
export default ParsedLink;

View File

@ -1,8 +1,12 @@
import React from "react";
import React, {useEffect} from "react";
function SearchBar(props) {
useEffect(() => {
document.getElementById("searchBar").value = props.defaultTerm
}, []);
const handleChange = (e) => {
props.changeParameter("title", e.target.value);
props.changeParameter("searchTerm", e.target.value);
};
return (
@ -11,12 +15,15 @@ function SearchBar(props) {
e.preventDefault();
}}
name="searchBar"
className="searchbar"
>
<input
id="searchBar"
autoComplete="off"
type="text"
name="searchTerm"
placeholder="Enter Book Name"
placeholder={"Search Book or Author"}
className="searchterm"
onChange={handleChange}
/>
</form>

View File

@ -2,15 +2,11 @@ import React from "react";
function SearchResult({ data }) {
return (
<div class="result">
<h3>
<a href={data.url} target="_blank">
{data.title}
<li className="result">
<a href={data.url} target="_blank" rel="noreferrer">
({data.lang.code}) {data.title}{data.author ? ` by ${data.author}` : ""}
</a>
</h3>
<h4>by {data.author ? data.author : "Unknown Author"}</h4>
<p>({data.lang.code})</p>
</div>
</li>
);
}

View File

@ -1,20 +1,20 @@
import React, { useState, useEffect, createContext } from "react";
import React, { useState, createContext } from "react";
//https://levelup.gitconnected.com/dark-mode-in-react-533faaee3c6e
export const themes = {
dark: "",
light: "white-content",
light: "dark-content",
};
export const swapMode = (theme) => {
switch (theme) {
case themes.light:
document.body.classList.add('white-content');
document.body.classList.add('dark-content');
break;
case themes.dark:
default:
document.body.classList.remove('white-content');
document.body.classList.remove('dark-content');
break;
}
}

21092
src/fpb.json

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB