Co-authored-by: Nick Quidas <nrfq@users.noreply.github.com>

pull/17/head
Brogan Clements 2022-04-03 17:12:52 -04:00
parent c3577ccbf7
commit 84b7a9d100
8 changed files with 3415 additions and 24277 deletions

6097
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,15 @@
"@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",
"react": "^17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-markdown": "^8.0.2",
"react-router-dom": "^6.3.0",
"react-scripts": "4.0.3",
"rehype-slug": "^5.0.1",
"web-vitals": "^1.1.2"
},
"scripts": {

View File

@ -31,7 +31,7 @@ h2 {
h3, h4, h5, h6 {
color:#494949;
font-weight: 500;
font-weight: bold;
}
a {
@ -339,7 +339,7 @@ a{
}
.result {
width: 30%;
/* width: 30%; */
padding: 0.25em 0.5em;
}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, createContext } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import LangFilters from "./components/LangFilters";
import SectDropdown from "./components/SectDropdown";
import SearchBar from "./components/SearchBar";
@ -6,72 +7,17 @@ import SearchResult from "./components/SearchResult";
import LightSwitch from "./components/LightSwitch";
import axios from "axios";
import Fuse from "fuse.js";
import { ThemeContext, themes, swapMode } from './darkMode';
import { useCookies } from 'react-cookie';
import { ThemeContext, themes, swapMode } from "./darkMode";
import { useCookies } from "react-cookie";
import Default from "./components/Default";
import SunImg from "./img/sun.png";
import MoonImg from "./img/moon.png";
import BookList from "./components/BookList";
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;
}
function jsonToArray(json) {
let arr = [];
let sections = [];
@ -112,8 +58,7 @@ function App() {
const [loading, setLoading] = useState(true); // Determines whether to show spinner
const [searchParams, setSearchParams] = useState({ searchTerm: "" });
const [searchResults, setSearchResults] = useState([]);
const [sectionResults, setSectionResults] = useState([]);
const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
const [cookies, setCookie, removeCookie] = useCookies(["lightMode"]);
const [lightMode, setLightMode] = useState(true);
@ -121,7 +66,6 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
const [error, setError] = useState("");
let resultsList = null; // the html string containing the search results
let sectionResultsList = null;
const changeParameter = (param, value) => {
// Lets a child component set the value of the search term
@ -130,7 +74,7 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
// fetches data the first time the page renders
useEffect(() => {
swapMode(cookies.lightMode ? themes.lightMode : themes.darkMode)
swapMode(cookies.lightMode ? themes.lightMode : themes.darkMode);
async function fetchData() {
try {
setLoading(true);
@ -154,7 +98,7 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
}, []);
// fires when searchTerm changes
// Finds most relevant title or author
// Finds most relevant title or author
// THIS IS THE MAIN SEARCH FUNCTION
useEffect(() => {
if (dataArray) {
@ -177,17 +121,17 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
if (value === null || 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
// this is because lang.code and section are strict filters
andQuery.push({ [key]: `^${value}` });
}
if(key === "searchTerm") {
orQuery.push({ "author": value});
orQuery.push({ "title": 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})
andQuery.push({ $or: orQuery });
// Perform the search
let result = fuse.search({
$and: andQuery,
@ -215,7 +159,7 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
}
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];
@ -251,15 +195,6 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
result = relevantLists.concat(result);
setSearchResults(result);
// console.log(result);
// No longer needed as the sections aren't being used
// 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);
}
}, [searchParams, dataArray]);
@ -276,128 +211,108 @@ const [cookies, setCookie, removeCookie] = useCookies(['lightMode']);
searchResults.map((entry) => {
return <SearchResult data={entry.item} />;
});
// Getting rid of the section results UI renders this irrelevant
// sectionResultsList =
// sectionResults &&
// sectionResults.map((section) => {
// return (
// <button
// onClick={() => {
// changeParameter("section", section);
// }}
// >
// {section}
// </button>
// );
// });
}
return (
<div
className="wrapper"
// style={{
// color: lightMode ? "black" : "white",
// backgroundColor: lightMode ? "white" : "black",
// }}
>
<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 ? MoonImg : SunImg}
onClick = {()=>{
setCookie("lightMode",willBeDarkMode);
changeTheme(willBeDarkMode ? themes.light : themes.dark)
}}
style={{width: "20px", height: "20px",display: "block",
marginLeft: "auto"
}}
/>)}
}
</ThemeContext.Consumer>
<header className="header">
<h1>
<a href="https://ebookfoundation.github.io/free-programming-books/" target="_blank" rel="noreferrer">
free-programming-books
</a>
</h1>
<p>
<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>
<SearchBar changeParameter={changeParameter} />
{/* Filters */}
<LangFilters changeParameter={changeParameter} data={data} />
{/* Keeping sections commented out just in case */}
{/* <SectDropdown
className="sect-drop"
changeParameter={changeParameter}
data={data}
value={searchParams["section"] || "allSects"}
/> */}
{/* {sectionResultsList && <h3>Suggestions based on your search</h3>}
<div className="search-results section-results">{sectionResultsList}</div> */}
</div>
</header>
<section className="search-results">
{resultsList ? (
<div>
<br />
<h2>Search Results</h2>
<ul>{resultsList}</ul>
</div>
) : searchParams.searchTerm ? (
<div>
<br />
<h2>No results found.</h2>
</div>
) : (
<Default />
)}
</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
<BrowserRouter>
<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 ? MoonImg : SunImg}
onClick={() => {
setCookie("lightMode", willBeDarkMode);
changeTheme(willBeDarkMode ? themes.light : themes.dark);
}}
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>
</small>
</p>
</footer>
</div>
</h1>
<p>
<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>
<SearchBar changeParameter={changeParameter} />
<LangFilters changeParameter={changeParameter} data={data} langCode={searchParams["lang.code"]} />
</div>
</header>
<section className="search-results">
{resultsList ? (
<div>
<br />
<h2>Search Results</h2>
<ul>{resultsList}</ul>
</div>
) : searchParams.searchTerm ? (
<div>
<br />
<h2>No results found.</h2>
</div>
) : (
<Routes>
<Route path="/free-programming-books-search" element={<Default />} />
<Route
path="/free-programming-books-search/books/:lang"
element={<BookList changeParameter={changeParameter} />}
/>
</Routes>
)}
</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>
</BrowserRouter>
);
}

View File

@ -0,0 +1,39 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import axios from "axios";
import { useParams } from "react-router-dom";
import rehypeSlug from 'rehype-slug'
function BookList({changeParameter}) {
let [markdown, setMarkdown] = useState(null);
const [loading, setLoading] = useState(true);
let params = useParams();
useEffect(() => {
changeParameter('lang.code', params.lang);
}, [params.lang]);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
let result = await axios.get(
`https://raw.githubusercontent.com/EbookFoundation/free-programming-books/main/books/free-programming-books-${params.lang}.md`
);
setMarkdown(result.data);
} catch (e) {
console.log("Couldn't get data. Please try again later")
}
setLoading(false);
}
fetchData();
}, []);
if(loading){
return <p>Loading...</p>
}
console.log(markdown);
return <section><ReactMarkdown children={markdown} rehypePlugins={[rehypeSlug]} /></section>;
}
export default BookList;

View File

@ -69,6 +69,135 @@ function Default() {
</li>
</ul>
<h2 id="resources">Resources</h2>
<p>This project lists books and other resources grouped by genres:</p>
<h3 id="books">Books</h3>
<p>
<a href="/free-programming-books-search/books/langs/">English, By Programming Language</a>
</p>
<p>
<a href="/free-programming-books-search/books/subjects/">English, By Subject</a>
</p>
<h4 id="other-languages">Other Languages</h4>
<ul>
<li>
<a href="/free-programming-books-search/books/ar/">Arabic / al arabiya / العربية</a>
</li>
<li>
<a href="/free-programming-books-search/books/az/">Azerbaijani / Азәрбајҹан дили / آذربايجانجا ديلي</a>
</li>
<li>
<a href="/free-programming-books-search/books/bn/">Bengali / </a>
</li>
<li>
<a href="/free-programming-books-search/books/bg/">Bulgarian / български</a>
</li>
<li>
<a href="/free-programming-books-search/books/my/">Burmese / </a>
</li>
<li>
<a href="/free-programming-books-search/books/zh/">Chinese / 中文</a>
</li>
<li>
<a href="/free-programming-books-search/books/cs/">Czech / čeština / český jazyk</a>
</li>
<li>
<a href="/free-programming-books-search/books/dk/">Danish / dansk</a>
</li>
<li>
<a href="/free-programming-books-search/books/nl/">Dutch / Nederlands</a>
</li>
<li>
<a href="/free-programming-books-search/books/et/">Estonian / eesti keel</a>
</li>
<li>
<a href="/free-programming-books-search/books/fi/">Finnish / suomi / suomen kieli</a>
</li>
<li>
<a href="/free-programming-books-search/books/fr/">French / français</a>
</li>
<li>
<a href="/free-programming-books-search/books/de/">German / Deutsch</a>
</li>
<li>
<a href="/free-programming-books-search/books/el/">Greek / ελληνικά</a>
</li>
<li>
<a href="/free-programming-books-search/books/he/">Hebrew / עברית</a>
</li>
<li>
<a href="/free-programming-books-search/books/hi/">Hindi / ि</a>
</li>
<li>
<a href="/free-programming-books-search/books/hu/">Hungarian / magyar / magyar nyelv</a>
</li>
<li>
<a href="/free-programming-books-search/books/id/">Indonesian / Bahasa Indonesia</a>
</li>
<li>
<a href="/free-programming-books-search/books/it/">Italian / italiano</a>
</li>
<li>
<a href="/free-programming-books-search/books/ja/">Japanese / 日本語</a>
</li>
<li>
<a href="/free-programming-books-search/books/ko/">Korean / 한국어 [韓國語]</a>
</li>
<li>
<a href="/free-programming-books-search/books/no/">Norwegian / Norsk</a>
</li>
<li>
<a href="/free-programming-books-search/books/fa_IR/">Persian / Farsi (Iran) / فارسى</a>
</li>
<li>
<a href="/free-programming-books-search/books/pl/">Polish / polski / język polski / polszczyzna</a>
</li>
<li>
<a href="/free-programming-books-search/books/pt_BR/">Portuguese (Brazil)</a>
</li>
<li>
<a href="/free-programming-books-search/books/pt_PT/">Portuguese (Portugal)</a>
</li>
<li>
<a href="/free-programming-books-search/books/ro/">Romanian (Romania) / limba română / român</a>
</li>
<li>
<a href="/free-programming-books-search/books/ru/">Russian / Русский язык</a>
</li>
<li>
<a href="/free-programming-books-search/books/sr/">Serbian / српски језик / srpski jezik</a>
</li>
<li>
<a href="/free-programming-books-search/books/sk/">Slovak / slovenčina</a>
</li>
<li>
<a href="/free-programming-books-search/books/es/">Spanish / español / castellano</a>
</li>
<li>
<a href="/free-programming-books-search/books/sv/">Swedish / Svenska</a>
</li>
<li>
<a href="/free-programming-books-search/books/ta/">Tamil / தமி</a>
</li>
<li>
<a href="/free-programming-books-search/books/th/">Thai / ไทย</a>
</li>
<li>
<a href="/free-programming-books-search/books/tr/">Turkish / Türkçe</a>
</li>
<li>
<a href="/free-programming-books-search/books/uk/">Ukrainian / Українська</a>
</li>
<li>
<a href="/free-programming-books-search/books/vi/">Vietnamese / Tiếng Việt</a>
</li>
</ul>
<h2 id="translations">Translations</h2>
<p>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
function LangFilters({ changeParameter, data }) {
function LangFilters({ changeParameter, data, langCode }) {
const [languages, setLanguages] = useState([]);
const [selected, setSelected] = useState("");
const [showFilters, setShow] = useState(false);
@ -11,6 +11,10 @@ function LangFilters({ changeParameter, data }) {
setSelected(e.target.value);
};
useEffect(() => {
setSelected(langCode);
}, [langCode]);
useEffect(
// run whenever data changes
() => {
@ -60,7 +64,14 @@ function LangFilters({ changeParameter, data }) {
let filterList = (
<form className="filters">
<label>
<input type="radio" key="all" className="sect-select" value="" onChange={handleChange} checked={"" == selected} />
<input
type="radio"
key="all"
className="sect-select"
value=""
onChange={handleChange}
checked={"" == selected}
/>
All Languages
</label>
{options}
@ -71,7 +82,7 @@ function LangFilters({ changeParameter, data }) {
<div className="langFilters">
<div className="filterHeader">
<h3>Filter by Language</h3>
<button onClick={() => setShow(!showFilters)}>{showFilters? "-" : "+"}</button>
<button onClick={() => setShow(!showFilters)}>{showFilters ? "-" : "+"}</button>
</div>
{showFilters ? filterList : ""}
</div>

21092
src/fpb.json

File diff suppressed because it is too large Load Diff