Merge branch 'EbookFoundation:main' into main
commit
806a9d6d46
|
@ -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"
|
97
README.md
97
README.md
|
@ -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 can’t go back!**
|
||||
2. This JSON is downloaded locally and searched locally when the actual search function is used.
|
||||
|
||||
If you aren’t 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 you’re 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 don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
349
src/App.css
349
src/App.css
|
@ -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;
|
||||
}
|
||||
|
|
417
src/App.js
417
src/App.js
|
@ -1,147 +1,108 @@
|
|||
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) => {
|
||||
document.sections.forEach((section) => {
|
||||
if (!sections.includes(section.section)) sections.push(section.section);
|
||||
section.entries.forEach((entry) => {
|
||||
arr.push({
|
||||
author: entry.author,
|
||||
title: entry.title,
|
||||
url: entry.url,
|
||||
lang: document.language,
|
||||
section: section.section,
|
||||
});
|
||||
});
|
||||
section.subsections.forEach((subsection) => {
|
||||
subsection.entries.forEach((entry) => {
|
||||
// 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,
|
||||
title: entry.title,
|
||||
url: entry.url,
|
||||
lang: document.language,
|
||||
section: section.section,
|
||||
subsection: subsection.section,
|
||||
});
|
||||
});
|
||||
// Add new entries that were under an h3 tag
|
||||
section.subsections.forEach((subsection) => {
|
||||
subsection.entries.forEach((entry) => {
|
||||
arr.push({
|
||||
author: entry.author,
|
||||
title: entry.title,
|
||||
url: entry.url,
|
||||
lang: document.language,
|
||||
section: section.section,
|
||||
subsection: subsection.section,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
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,98 +110,222 @@ 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}` });
|
||||
}
|
||||
if (key === "searchTerm") {
|
||||
orQuery.push({ author: value });
|
||||
orQuery.push({ title: value });
|
||||
}
|
||||
query.push({ [key]: 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" >
|
||||
<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}
|
||||
onClick = {()=>{
|
||||
setCookie("lightMode",willBeDarkMode);
|
||||
changeTheme(willBeDarkMode ? themes.light : themes.dark)
|
||||
}}
|
||||
style={{width: "20px", height: "20px",display: "block",
|
||||
marginLeft: "auto"
|
||||
}}
|
||||
/>)}
|
||||
}
|
||||
</ThemeContext.Consumer>
|
||||
|
||||
<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.
|
||||
</p>
|
||||
)}
|
||||
<div className="search-results">{sectionResultsList}</div>
|
||||
<h2>Top Results</h2>
|
||||
<div className="search-results">{resultsList}</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<ThemeContext.Consumer>
|
||||
{({ changeTheme }) => {
|
||||
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);
|
||||
}}
|
||||
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>
|
||||
|
||||
<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>
|
||||
{loading ? (
|
||||
<p />
|
||||
) : (
|
||||
<div>
|
||||
<SearchBar changeParameter={changeParameter} defaultTerm={searchParams.searchTerm} />{" "}
|
||||
<LangFilters changeParameter={changeParameter} data={data} langCode={searchParams["lang.code"]} />{" "}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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/?§=${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;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
21092
src/fpb.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
Loading…
Reference in New Issue