├── .browserslistrc ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── appListeners.js ├── appRoutes.js ├── appState.js ├── appUtilities.js ├── config.example.json ├── fonts │ └── fontello │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── css │ │ ├── animation.css │ │ ├── icons-codes.css │ │ ├── icons-embedded.css │ │ ├── icons-ie7-codes.css │ │ ├── icons-ie7.css │ │ └── icons.css │ │ ├── demo.html │ │ ├── font │ │ ├── icons.eot │ │ ├── icons.svg │ │ ├── icons.ttf │ │ ├── icons.woff │ │ └── icons.woff2 │ │ └── fontello-config.json ├── i18n.js ├── images │ ├── logo.svg │ └── social.svg ├── index.html ├── index.js ├── manifest.webmanifest ├── styles │ ├── _search.scss │ ├── index.scss │ └── picnic-customizations │ │ ├── _custom.scss │ │ └── theme │ │ ├── _colors.scss │ │ └── _theme.scss └── views │ ├── 404.js │ ├── about.js │ ├── controller.js │ ├── global.js │ ├── home │ ├── controller.js │ ├── index.js │ ├── loggedIn.js │ └── loggedOut.js │ ├── login │ ├── controller.js │ └── index.js │ ├── partials │ ├── modal.js │ ├── reviewCard.js │ └── starRating.js │ ├── search │ ├── controller.js │ ├── index.js │ └── resultDetails.js │ └── shelves │ ├── controller.js │ ├── index.js │ ├── shelf.js │ └── userShelves │ ├── editModal.js │ └── index.js ├── package.json ├── process-images.js ├── server ├── config.example.json ├── controllers │ ├── account.js │ ├── bookData │ │ ├── Inventaire.js │ │ └── index.js │ ├── bookReference.js │ ├── search │ │ ├── Inventaire.js │ │ └── index.js │ └── shelf.js ├── fastify-plugins │ ├── fastify-nodemailer.js │ └── fastify-sequelize.js ├── i18n │ ├── index.js │ ├── locales │ │ └── en │ │ │ ├── pages │ │ │ ├── about.md │ │ │ └── community.md │ │ │ └── ui.json │ └── routes.js ├── index.js ├── routes │ ├── account.js │ ├── books.js │ ├── public.js │ ├── search.js │ └── shelf.js ├── sequelize │ ├── associations │ │ ├── BookReference.js │ │ ├── Recommendation.js │ │ ├── Review.js │ │ ├── Shelf.js │ │ ├── ShelfItem.js │ │ ├── Status.js │ │ ├── User.js │ │ └── index.js │ ├── db-diagram.svg │ ├── generate-db-diagram.js │ ├── migration.js │ ├── models │ │ ├── BookReference.js │ │ ├── Follow.js │ │ ├── PermissionLevel.js │ │ ├── Reaction.js │ │ ├── Recommendation.js │ │ ├── Review.js │ │ ├── Shelf.js │ │ ├── ShelfItem.js │ │ ├── Status.js │ │ ├── User.js │ │ └── index.js │ └── setup-database.js └── templates │ ├── email.confirm_account.txt │ └── email.confirm_account_thanks.txt └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | >1% 2 | last 4 versions 3 | Firefox ESR 4 | not ie < 9 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | .cache/ 4 | dist/ 5 | dev/ 6 | 7 | **/*.log 8 | config.json 9 | *.sqlite* 10 | *.db 11 | .dbversion 12 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Discussion 9 | 10 | - Join the [Gitter community](https://gitter.im/Readlebee/community) to chat about the project or give feedback 11 | - Add [Issues](https://gitlab.com/Alamantus/Readlebee/issues) for feature requests or bug reports 12 | 13 | ## Pull Request Process 14 | 15 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 16 | build. 17 | 2. Update the README.md with details of changes to the interface, this includes new environment 18 | variables, exposed ports, useful file locations and container parameters. 19 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 20 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 21 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 22 | do not have permission to do that, you may request the second reviewer to merge it for you. 23 | 24 | ## Code of Conduct 25 | 26 | ### Our Pledge 27 | 28 | In the interest of fostering an open and welcoming environment, we as 29 | contributors and maintainers pledge to making participation in our project and 30 | our community a harassment-free experience for everyone, regardless of age, body 31 | size, disability, ethnicity, gender identity and expression, level of experience, 32 | nationality, personal appearance, race, religion, or sexual identity and 33 | orientation. 34 | 35 | ### Our Standards 36 | 37 | Examples of behavior that contributes to creating a positive environment 38 | include: 39 | 40 | * Using welcoming and inclusive language 41 | * Being respectful of differing viewpoints and experiences 42 | * Gracefully accepting constructive criticism 43 | * Focusing on what is best for the community 44 | * Showing empathy towards other community members 45 | 46 | Examples of unacceptable behavior by participants include: 47 | 48 | * The use of sexualized language or imagery and unwelcome sexual attention or 49 | advances 50 | * Trolling, insulting/derogatory comments, and personal or political attacks 51 | * Public or private harassment 52 | * Publishing others' private information, such as a physical or electronic 53 | address, without explicit permission 54 | * Other conduct which could reasonably be considered inappropriate in a 55 | professional setting 56 | 57 | ### Our Responsibilities 58 | 59 | Project maintainers are responsible for clarifying the standards of acceptable 60 | behavior and are expected to take appropriate and fair corrective action in 61 | response to any instances of unacceptable behavior. 62 | 63 | Project maintainers have the right and responsibility to remove, edit, or 64 | reject comments, commits, code, wiki edits, issues, and other contributions 65 | that are not aligned to this Code of Conduct, or to ban temporarily or 66 | permanently any contributor for other behaviors that they deem inappropriate, 67 | threatening, offensive, or harmful. 68 | 69 | ### Scope 70 | 71 | This Code of Conduct applies both within project spaces and in public spaces 72 | when an individual is representing the project or its community. Examples of 73 | representing a project or community include using an official project e-mail 74 | address, posting via an official social media account, or acting as an appointed 75 | representative at an online or offline event. Representation of a project may be 76 | further defined and clarified by project maintainers. 77 | 78 | ### Enforcement 79 | 80 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 81 | reported by contacting Alamantus by PM on Gitter (https://gitter.io/Alamantus). All 82 | complaints will be reviewed and investigated and will result in a response that 83 | is deemed necessary and appropriate to the circumstances. The project team is 84 | obligated to maintain confidentiality with regard to the reporter of an incident. 85 | Further details of specific enforcement policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership. 90 | 91 | ### Attribution 92 | 93 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 94 | available at [http://contributor-covenant.org/version/1/4][version] 95 | 96 | [homepage]: http://contributor-covenant.org 97 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readlebee 2 | 3 | --- 4 | 5 | ## ⚠ This project is [no longer maintained](https://floss.social/@Readlebee/108408389047896535) ⚠ 6 | 7 | If you are interested in taking over Readlebee's development and Mastodon account, please contact Robbie Antenesse ([@robbie@antenesse.ml](https://antenesse.ml/profile/robbie)) via an account in [the Fediverse](https://the-federation.info)! 8 | 9 | --- 10 | 11 | [![Read our Contribution Guidelines](https://badges.frapsoft.com/os/v1/open-source.svg?v=102)](./CONTRIBUTING.md) 12 | 13 | An attempt at a viable alternative to Goodreads 14 | 15 | ## Important Links 16 | 17 | - [Project Scope](https://gitlab.com/Alamantus/Readlebee/wikis/Project-Scope) 18 | - Features we feel are essential to the project. Anything beyond the scope should be discussed for later and not prioritized. 19 | - [Dependencies Stack](https://gitlab.com/Alamantus/Readlebee/wikis/Dependencies-Stack) 20 | - A list of dependencies used in the project and a short explanation of what each of them are for. 21 | - [Contribution Guidelines](./CONTRIBUTING.md) 22 | - Subject to change but important to follow. Includes a basic code of conduct. 23 | - [Project chat via Gitter](https://gitter.io/Readlebee) 24 | - Real-time discussion about the project. 25 | - [Issue Tracker](https://gitlab.com/Alamantus/Readlebee/issues) 26 | - For adding and tracking feature requests, feedback, and bug reports. 27 | - [Main Repo on GitLab](https://gitlab.com/Alamantus/Readlebee) 28 | - Where all changes are made "official". 29 | - [Mirror Repo on GitHub](https://github.com/Alamantus/Readlebee) 30 | - Gets changes from GitLab pushed to it so people who prefer GitHub can contribute there as well. Pull requests and issues created here will also be addressed. 31 | 32 | ## Development 33 | 34 | ### Requirements 35 | 36 | - [Git](https://git-scm.com/) 37 | - [NodeJS 10.16.x](https://nodejs.org/) 38 | - [PostgreSQL 11](https://www.postgresql.org/download/) 39 | - See the following articles for guidance on how to install it: 40 | - Windows: http://www.postgresqltutorial.com/install-postgresql/ 41 | - Ubuntu: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04 42 | - Be sure you set up an account for a readlebee database and [set a password for the account](https://stackoverflow.com/a/12721095) 43 | - I plan to write up a tutorial on how to get this set up in the wiki at some point soon by combining these links 44 | 45 | ### Installation 46 | 47 | To develop, you'll need to know how to use a terminal or shell on your computer. 48 | 49 | Clone the repo to your computer with [Git](https://git-scm.com/) by running: 50 | 51 | ``` 52 | git clone https://gitlab.com/Alamantus/Readlebee.git 53 | ``` 54 | 55 | Then run use [Yarn](https://yarnpkg.com) to install the dependencies: 56 | 57 | ``` 58 | yarn 59 | ``` 60 | 61 | Alternatively, you can use the NPM that's included with Node: 62 | 63 | ``` 64 | npm install 65 | ``` 66 | 67 | Once installed, make sure that the images are processed from their original form: 68 | 69 | ``` 70 | npm run process-images 71 | ``` 72 | 73 | Finally, copy the `config.example.json` files in `app/` and `server/` folders into a new `config.json` file in each. 74 | These config files are not saved to the project, so you can put your server/database info in there safely. 75 | 76 | ## Usage 77 | 78 | After everything's installed, run the "dev" NPM script to build and watch the front end and run the back end: 79 | 80 | ``` 81 | npm run dev 82 | ``` 83 | 84 | Then use your browser to navigate to http://localhost:3000 to view the website. 85 | 86 | When you make a change, you need to stop the server with `Ctrl+C` and re-run the script. 87 | 88 | It's early days, so this segment will definitely change later as the project gets more complex. 89 | 90 | --- 91 | 92 | ## Production 93 | 94 | This is totally not yet ready, but I want to use this space to block out what how I would like the installation process 95 | to go for people installing the app. 96 | 97 | ### Requirements 98 | 99 | - NodeJS v8.14+ 100 | - NPM v6.4.1+ 101 | - NGINX 102 | - PostgreSQL 11+ 103 | 104 | ### Recommendations 105 | 106 | - Use a Debian 9 server for stability. Ubuntu should also work just fine. 107 | - Use the default apt packages for the requirements 108 | - Use Git to download the project for installation and easy upgrading 109 | 110 | ### Installation 111 | 112 | Here's a step-by-step installation process so you can get a grasp of what you need to do from a brand new 113 | Debian 9 installation (not included in steps). Ubuntu installation should be more or less exactly the same. 114 | 115 | #### Step 1: Install Requirements 116 | 117 | Install the requirements with the following commands (note: you may need to use `sudo` for each of these commands): 118 | 119 | ``` 120 | sudo apt update 121 | sudo apt install nodejs nginx postgres-11 122 | ``` 123 | 124 | And optionally (but recommended): 125 | ``` 126 | sudo apt install git 127 | ``` 128 | 129 | Follow any instructions during each of the installations to get the programs set up correctly. 130 | 131 | NGINX may require additional setup, so check through [this page](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-debian-9) for different things that might be good to do. 132 | 133 | PostgreSQL will need a database created, so do that and make a user that can access it that's not the root user. 134 | 135 | #### Step 2: Download the Project 136 | 137 | You can set up the project folder in any location on your server, but these instructions will set it up in 138 | the current user's home folder using Git like so: 139 | 140 | ``` 141 | cd ~ 142 | git clone https://gitlab.com/Alamantus/Readlebee.git && cd Readlebee 143 | ``` 144 | 145 | This will download the entire project source code into a `Readlebee` folder. 146 | 147 | #### Step 3: Configure the Project 148 | 149 | Next, There are some configurations you need to set up. Rename the `config.example.json` to `config.json` like so: 150 | 151 | ``` 152 | mv config.example.json config.json 153 | ``` 154 | 155 | And edit its contents with the correct data for your server using your text editor of choice. Here is what 156 | the `config.example.json` looks like with some explanations of each field: 157 | 158 | ``` 159 | { 160 | "port": 3000 # the port that the server will serve the app from. 161 | "dbhost": "localhost" # Where the postgres server is 162 | "dbport": 5432 # What port the postgres server uses 163 | "dbname": "Readlebee" # The name of the database Readlebee will use to make tables and store data in 164 | "dbuser": "root" # The username with access to your postgres database 165 | "dbpass": "password" # The password for the username above 166 | ... # more to come 167 | } 168 | ``` 169 | 170 | #### Step 4: Install the Project 171 | 172 | You will then need to install the project. 173 | 174 | ``` 175 | sudo npm install 176 | ``` 177 | 178 | This will install all of the dependencies, compile all of the Sass into usable CSS, set up the database and tables in PostgreSQL, 179 | and do any other things that need to be done to get the project set up and usable. 180 | 181 | #### Step 5: Run it! 182 | 183 | Run the following to start the server: 184 | 185 | ``` 186 | sudo npm start 187 | ``` 188 | 189 | Then it'll be running on your server's localhost at the port you specified in the config! 190 | 191 | #### Step 6: Set up an NGINX reverse proxy 192 | 193 | Set up a reverse proxy to your localhost:proxy. This'll have to get filled in later. 194 | -------------------------------------------------------------------------------- /app/appListeners.js: -------------------------------------------------------------------------------- 1 | const appListeners = (app, state, emitter) => { 2 | emitter.on(state.events.DOMCONTENTLOADED, () => { 3 | emitter.emit(state.events.DOMTITLECHANGE, app.siteConfig.siteName); 4 | 5 | // Emitter listeners 6 | emitter.on(state.events.RENDER, callback => { 7 | // This is a dirty hack to get the callback to call *after* re-rendering. 8 | if (callback && typeof callback === "function") { 9 | setTimeout(() => { 10 | callback(); 11 | }, 50); 12 | } 13 | }); 14 | 15 | emitter.on(state.events.SET_LANGUAGE, newLanguage => { 16 | app.setSettingsItem('lang', newLanguage); 17 | state.language = newLanguage; 18 | state.i18n.fetchLocaleUI().then(() => { 19 | emitter.emit(state.events.RENDER); 20 | }); 21 | }); 22 | 23 | emitter.on(state.events.ADD_TO_SHELF, async (book, shelfId, callback = () => {}) => { 24 | let bookId; 25 | if(typeof book.source !== 'undefined' && typeof book.uri !== 'undefined') { 26 | const bookSearchResult = await fetch('/api/books/getId', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify(book), 32 | }).then(response => response.json()); 33 | 34 | if (typeof bookSearchResult.error !== 'undefined') { 35 | console.error(bookSearchResult); 36 | return bookSearchResult; 37 | } 38 | 39 | bookId = bookSearchResult; 40 | } else { 41 | bookId = book.id; 42 | } 43 | 44 | return fetch('/api/shelf/addItem', { 45 | method: 'POST', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify({ 50 | shelfId, 51 | bookId, 52 | }), 53 | }).then(result => callback(result)); 54 | }); 55 | 56 | if (state.isFrontend) { 57 | state.i18n.fetchLocaleUI().then(() => { 58 | app.checkIfLoggedIn(state).then(isLoggedIn => { 59 | emitter.emit(state.events.RENDER); // This should hopefully only run once after the DOM is loaded. It prevents routing issues where 'render' hasn't been defined yet 60 | }); 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | module.exports = { appListeners }; 67 | -------------------------------------------------------------------------------- /app/appRoutes.js: -------------------------------------------------------------------------------- 1 | const { globalView } = require('./views/global'); 2 | const { homeView } = require('./views/home'); 3 | const { aboutView } = require('./views/about'); 4 | const { loginView } = require('./views/login'); 5 | const { searchView } = require('./views/search'); 6 | const { shelvesView } = require('./views/shelves'); 7 | const { errorView } = require('./views/404'); 8 | 9 | const appRoutes = (app) => { 10 | app.route('/', (state, emit) => globalView(state, emit, homeView)); 11 | 12 | app.route('/about', (state, emit) => globalView(state, emit, aboutView)); 13 | 14 | app.route('/login', (state, emit) => globalView(state, emit, loginView)); 15 | 16 | app.route('/logout', () => window.location.reload()); // If Choo navigates here, refresh the page instead so the server can handle it and log out 17 | 18 | app.route('/search', (state, emit) => globalView(state, emit, searchView)); 19 | 20 | app.route('/shelves', (state, emit) => globalView(state, emit, shelvesView)); 21 | 22 | app.route('/404', (state, emit) => globalView(state, emit, errorView)); 23 | } 24 | 25 | module.exports = { appRoutes }; 26 | -------------------------------------------------------------------------------- /app/appState.js: -------------------------------------------------------------------------------- 1 | const { I18n } = require("./i18n"); 2 | 3 | const appState = (app, state, emitter) => { 4 | state.isFrontend = typeof window !== 'undefined'; 5 | 6 | state.events.SET_LANGUAGE = 'setLanguage'; 7 | state.events.ADD_TO_SHELF = 'addToShelf'; 8 | 9 | if (state.isFrontend) { 10 | state.language = app.getSettingsItem('lang') ? app.getSettingsItem('lang') : (window.navigator.language || window.navigator.userLanguage).split('-')[0]; 11 | state.isLoggedIn = false; 12 | state.i18n = new I18n(state); // Global I18n class passed to all views 13 | } 14 | state.viewStates = {}; 15 | } 16 | 17 | module.exports = { appState }; 18 | -------------------------------------------------------------------------------- /app/appUtilities.js: -------------------------------------------------------------------------------- 1 | const appUtilities = (app) => { 2 | app.getSettingsItem = settingsKey => { 3 | let savedSettings = app.state.isFrontend && window.localStorage.getItem('settings'); 4 | if (savedSettings) { 5 | savedSettings = JSON.parse(savedSettings); 6 | if (typeof savedSettings[settingsKey] !== 'undefined') { 7 | return savedSettings[settingsKey]; 8 | } 9 | } 10 | return null; 11 | } 12 | app.setSettingsItem = (settingsKey, value) => { 13 | if (!app.state.isFrontend) return null; 14 | 15 | let savedSettings = window.localStorage.getItem('settings'); 16 | if (savedSettings) { 17 | savedSettings = JSON.parse(savedSettings); 18 | } else { 19 | savedSettings = {}; 20 | } 21 | savedSettings[settingsKey] = value; 22 | return window.localStorage.setItem('settings', JSON.stringify(savedSettings)); 23 | } 24 | 25 | app.checkIfLoggedIn = (appState) => { 26 | if (!app.state.isFrontend) return false; 27 | 28 | return fetch('/api/account/validate', { method: 'post' }) 29 | .then(response => response.json()) 30 | .then(response => { 31 | if (response.error !== false) { 32 | console.warn(appState.i18n.__(response.message)); 33 | return false; 34 | } 35 | 36 | console.info(appState.i18n.__(response.message)); 37 | appState.isLoggedIn = true; 38 | return true; 39 | }); 40 | } 41 | } 42 | 43 | module.exports = { appUtilities }; 44 | -------------------------------------------------------------------------------- /app/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteName": "Readlebee" 3 | } -------------------------------------------------------------------------------- /app/fonts/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | ## Typicons 14 | 15 | (c) Stephen Hutchings 2012 16 | 17 | Author: Stephen Hutchings 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://typicons.com/ 20 | 21 | 22 | ## Iconic 23 | 24 | Copyright (C) 2012 by P.J. Onori 25 | 26 | Author: P.J. Onori 27 | License: SIL (http://scripts.sil.org/OFL) 28 | Homepage: http://somerandomdude.com/work/iconic/ 29 | 30 | 31 | ## Fontelico 32 | 33 | Copyright (C) 2012 by Fontello project 34 | 35 | Author: Crowdsourced, for Fontello project 36 | License: SIL (http://scripts.sil.org/OFL) 37 | Homepage: http://fontello.com 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/fonts/fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | 278 | 279 | 291 | 292 | 293 |
294 |

icons font demo

295 | 298 |
299 |
300 |
301 |
icon-star0xe800
302 |
icon-star-empty0xe801
303 |
icon-chat0xe802
304 |
icon-heart-outline0xe803
305 |
306 |
307 |
icon-heart-filled0xe804
308 |
icon-comment0xe805
309 |
icon-reload0xe806
310 |
icon-check0xe807
311 |
312 |
313 |
icon-plus0xe808
314 |
icon-search0xe809
315 |
icon-delete0xe80a
316 |
icon-edit0xe80b
317 |
318 |
319 |
icon-close0xe80c
320 |
icon-loading0xe839
321 |
icon-external0xf08e
322 |
icon-star-half0xf123
323 |
324 |
325 | 326 | 327 | -------------------------------------------------------------------------------- /app/fonts/fontello/font/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alamantus/Readlebee/0123251b383d277e50ff4f7fa33ccc8d35d47c93/app/fonts/fontello/font/icons.eot -------------------------------------------------------------------------------- /app/fonts/fontello/font/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2019 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/fonts/fontello/font/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alamantus/Readlebee/0123251b383d277e50ff4f7fa33ccc8d35d47c93/app/fonts/fontello/font/icons.ttf -------------------------------------------------------------------------------- /app/fonts/fontello/font/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alamantus/Readlebee/0123251b383d277e50ff4f7fa33ccc8d35d47c93/app/fonts/fontello/font/icons.woff -------------------------------------------------------------------------------- /app/fonts/fontello/font/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alamantus/Readlebee/0123251b383d277e50ff4f7fa33ccc8d35d47c93/app/fonts/fontello/font/icons.woff2 -------------------------------------------------------------------------------- /app/fonts/fontello/fontello-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icons", 3 | "css_prefix_text": "icon-", 4 | "css_use_suffix": false, 5 | "hinting": true, 6 | "units_per_em": 1000, 7 | "ascent": 850, 8 | "glyphs": [ 9 | { 10 | "uid": "e15f0d620a7897e2035c18c80142f6d9", 11 | "css": "external", 12 | "code": 61582, 13 | "src": "fontawesome" 14 | }, 15 | { 16 | "uid": "9bc2902722abb366a213a052ade360bc", 17 | "css": "loading", 18 | "code": 59449, 19 | "src": "fontelico" 20 | }, 21 | { 22 | "uid": "3b00728aa97ad1a2581d414bd9d650bc", 23 | "css": "heart-outline", 24 | "code": 59395, 25 | "src": "typicons" 26 | }, 27 | { 28 | "uid": "hi76m8qggwn5lbl286oeqp64q0n8kusy", 29 | "css": "heart-filled", 30 | "code": 59396, 31 | "src": "typicons" 32 | }, 33 | { 34 | "uid": "43fl9m553j1z5937vfjz0lgolrlspxwl", 35 | "css": "check", 36 | "code": 59399, 37 | "src": "typicons" 38 | }, 39 | { 40 | "uid": "1gf923f9wvaezxmfon515dglxa3drf0e", 41 | "css": "plus", 42 | "code": 59400, 43 | "src": "typicons" 44 | }, 45 | { 46 | "uid": "474656633f79ea2f1dad59ff63f6bf07", 47 | "css": "star", 48 | "code": 59392, 49 | "src": "fontawesome" 50 | }, 51 | { 52 | "uid": "d17030afaecc1e1c22349b99f3c4992a", 53 | "css": "star-empty", 54 | "code": 59393, 55 | "src": "fontawesome" 56 | }, 57 | { 58 | "uid": "84cf1fcc3fec556e7eaeb19679ca2dc9", 59 | "css": "star-half", 60 | "code": 61731, 61 | "src": "fontawesome" 62 | }, 63 | { 64 | "uid": "ccf71c505b173c61a2e4e8c8cb907dfa", 65 | "css": "chat", 66 | "code": 59394, 67 | "src": "typicons" 68 | }, 69 | { 70 | "uid": "b90868gfogj970a1g0dnot6hm5r4uj55", 71 | "css": "comment", 72 | "code": 59397, 73 | "src": "typicons" 74 | }, 75 | { 76 | "uid": "a73c5deb486c8d66249811642e5d719a", 77 | "css": "reload", 78 | "code": 59398, 79 | "src": "fontawesome" 80 | }, 81 | { 82 | "uid": "c6344a6ed148da12354cc90705287696", 83 | "css": "search", 84 | "code": 59401, 85 | "src": "iconic" 86 | }, 87 | { 88 | "uid": "jqzwo6i8oicjbn049sh2856d8anrqoli", 89 | "css": "edit", 90 | "code": 59403, 91 | "src": "typicons" 92 | }, 93 | { 94 | "uid": "csuoy0rqoun3unhsgjoy2uumpldzbfmt", 95 | "css": "delete", 96 | "code": 59402, 97 | "src": "typicons" 98 | }, 99 | { 100 | "uid": "1dq4tek4k8ea7zlj4kc3w83itnutaxg5", 101 | "css": "close", 102 | "code": 59404, 103 | "src": "typicons" 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | class I18n { 2 | constructor(appState) { 3 | this.appState = appState; 4 | this.availableLanguages = null; 5 | this.language = null; 6 | this.default = null; 7 | this.pages = {}; 8 | } 9 | 10 | get needsFetch () { 11 | return !this.availableLanguages && !this.language && !this.default; 12 | } 13 | 14 | fetchLocaleUI () { 15 | return fetch(`/locales/${this.appState.language}/ui`).then(response => response.json()).then(response => { 16 | this.availableLanguages = response.available; 17 | this.default = response.default; 18 | this.language = response.locale; 19 | }).catch(error => { 20 | console.error(error); 21 | }); 22 | } 23 | 24 | fetchLocalePage (page) { 25 | return fetch(`/locales/${this.appState.language}/page/${page}`).then(response => response.text()).then(response => { 26 | this.pages[page] = response; 27 | }).catch(error => { 28 | console.error(error); 29 | }); 30 | } 31 | 32 | translate (target, useDefault = false) { 33 | let language = useDefault ? this.default : this.language; 34 | const pieces = target.split('.'); 35 | 36 | if (!this.needsFetch && !useDefault && this.appState.language !== language.locale) { 37 | console.warn(`The target language (${this.appState.language}) does not exist. Defaulting to ${this.default.name} (${this.default.locale}).`); 38 | language = this.default; 39 | } 40 | 41 | let translation = pieces.reduce((lang, piece, i) => { 42 | if (lang === false) return false; 43 | 44 | if (typeof lang[piece] === 'object') { 45 | // Only continue if there's another piece, otherwise, it will error. 46 | if (typeof pieces[i + 1] !== 'undefined') { 47 | return lang[piece]; 48 | } 49 | } 50 | 51 | if (typeof lang[piece] === 'string') { 52 | return lang[piece]; 53 | } 54 | 55 | return false; 56 | }, Object.assign({}, language)); 57 | 58 | if (translation === false) { 59 | console.log(this); 60 | if (language.locale !== this.default.locale) { 61 | console.warn(`The translation for "${target}" is not set in the ${this.language.locale} locale. Using ${this.default.name} (${this.default.locale}) instead.`); 62 | return this.translate(target, true); 63 | } 64 | console.error(`The translation for "${target}" is set up in neither the target nor default locale.`); 65 | return target; 66 | } 67 | 68 | return translation; 69 | } 70 | 71 | __ (translation) { 72 | return this.translate(translation); 73 | } 74 | } 75 | 76 | module.exports = { I18n }; 77 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Readlebee 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | 3 | const choo = require('choo'); 4 | 5 | const config = require('./config.json'); 6 | const { appRoutes } = require('./appRoutes'); 7 | const { appListeners } = require('./appListeners'); 8 | const { appState } = require('./appState.js'); 9 | const { appUtilities } = require('./appUtilities.js'); 10 | 11 | function frontend() { 12 | const app = choo(); 13 | 14 | if (process.env.NODE_ENV !== 'production') { 15 | // Only runs in development and will be stripped from production build. 16 | app.use(require('choo-devtools')()); // Exposes `choo` to the console for debugging! 17 | } 18 | 19 | app.use((state, emitter) => { 20 | app.siteConfig = config; 21 | appUtilities(app); 22 | }); 23 | 24 | app.use((state, emitter) => { 25 | appState(app, state); 26 | 27 | // Listeners 28 | appListeners(app, state, emitter); 29 | }); 30 | 31 | // Routes 32 | appRoutes(app); 33 | 34 | app.mount('body'); // Overwrite the `` tag with the content of the Choo app 35 | 36 | return app; 37 | } 38 | 39 | if (typeof window !== 'undefined') { 40 | frontend(); 41 | } 42 | 43 | module.exports = frontend; 44 | -------------------------------------------------------------------------------- /app/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Readlebee", 3 | "short_name": "Readlebee", 4 | "icons": [ 5 | { 6 | "src": "../dev/images/icon-128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "../dev/images/icon-144.png", 12 | "sizes": "144x144", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "../dev/images/icon-152.png", 17 | "sizes": "152x152", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "../dev/images/icon-192.png", 22 | "sizes": "192x192", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "../dev/images/icon-256.png", 27 | "sizes": "256x256", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "../dev/images/icon-512.png", 32 | "sizes": "512x512", 33 | "type": "image/png" 34 | } 35 | ], 36 | "start_url": "/", 37 | "display": "standalone", 38 | "orientation": "portrait", 39 | "background_color": "#000000", 40 | "theme_color": "#1C4AFF" 41 | } -------------------------------------------------------------------------------- /app/styles/_search.scss: -------------------------------------------------------------------------------- 1 | .search-result { 2 | .search-image { 3 | width: 100%; 4 | max-width: 300px; 5 | } 6 | } -------------------------------------------------------------------------------- /app/styles/index.scss: -------------------------------------------------------------------------------- 1 | //! Picnic CSS http://www.picnicss.com/ 2 | 3 | // Imports the base variable styles 4 | @import './picnic-customizations/theme/theme'; 5 | 6 | @import '../../node_modules/picnic/src/vendor/compass-breakpoint/stylesheets/breakpoint'; 7 | 8 | // Normalize.css (external library) 9 | @import '../../node_modules/picnic/src/plugins/normalize/plugin'; 10 | 11 | // Generic styles for things like , and others 12 | // It also overwrites normalize.css a bit 13 | @import '../../node_modules/picnic/src/plugins/generic/plugin'; 14 | @import '../../node_modules/picnic/src/plugins/fontello/plugin'; 15 | 16 | // Simple elements 17 | @import '../../node_modules/picnic/src/plugins/label/plugin'; 18 | @import '../../node_modules/picnic/src/plugins/button/plugin'; 19 | 20 | // Forms 21 | @import '../../node_modules/picnic/src/plugins/input/plugin'; 22 | @import '../../node_modules/picnic/src/plugins/select/plugin'; 23 | @import '../../node_modules/picnic/src/plugins/radio/plugin'; 24 | @import '../../node_modules/picnic/src/plugins/checkbox/plugin'; 25 | 26 | // Components 27 | @import '../../node_modules/picnic/src/plugins/table/plugin'; 28 | @import '../../node_modules/picnic/src/plugins/grid/plugin'; 29 | 30 | // Extra 31 | @import '../../node_modules/picnic/src/plugins/nav/plugin'; 32 | 33 | @import '../../node_modules/picnic/src/plugins/stack/plugin'; 34 | @import '../../node_modules/picnic/src/plugins/card/plugin'; 35 | @import '../../node_modules/picnic/src/plugins/modal/plugin'; 36 | 37 | // @import '../../node_modules/picnic/src/plugins/dropimage/plugin'; 38 | @import '../../node_modules/picnic/src/plugins/tabs/plugin'; 39 | @import '../../node_modules/picnic/src/plugins/tooltip/plugin'; 40 | 41 | // Custom global styling 42 | @import './picnic-customizations/custom'; 43 | 44 | // View styling 45 | @import './search'; 46 | 47 | // Icons 48 | @import '../fonts/fontello/css/animation.css'; 49 | @import '../fonts/fontello/css/icons.css'; 50 | -------------------------------------------------------------------------------- /app/styles/picnic-customizations/_custom.scss: -------------------------------------------------------------------------------- 1 | // Picnic Overrides 2 | body { 3 | font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 4 | } 5 | 6 | a { 7 | color: $picnic-info; 8 | 9 | &:not(.button) { 10 | &:hover, 11 | &:active, 12 | &:focus { 13 | color: darken($picnic-info, 10); 14 | } 15 | } 16 | } 17 | 18 | nav { 19 | position: relative; 20 | } 21 | 22 | // For some reason, hidden is not important by default! 23 | [hidden] { 24 | display: none !important; 25 | } 26 | 27 | .pseudo[data-tooltip]::after { 28 | background-color: $picnic-black; 29 | color: $picnic-white; 30 | } 31 | 32 | .modal { 33 | position: absolute; // Prevent modal partial from interrupting content flow. 34 | } 35 | 36 | // External links 37 | a[href^="http://"]:after, 38 | a[href^="https://"]:after{ 39 | font-family: "icons"; 40 | font-size: 70%; 41 | vertical-align: top; 42 | margin-left: 3px; 43 | content: "\f08e"; 44 | } 45 | 46 | // New components 47 | .menu ul li { 48 | display: inline-block; 49 | list-style: none; 50 | } 51 | 52 | footer nav { 53 | .links { 54 | @extend .brand; 55 | font-weight: unset; 56 | } 57 | } 58 | 59 | .container { 60 | display: block; 61 | width: 75%; 62 | max-width: 900px; 63 | min-width: 560px; 64 | margin: 0 auto; 65 | padding: $picnic-separation 0; 66 | 67 | &.wide { 68 | width: 100%; 69 | padding: $picnic-separation; 70 | } 71 | } 72 | 73 | .title + .subtitle { 74 | margin-top: -2 * $picnic-separation; 75 | padding-top: 0; 76 | font-size: 0.8em; 77 | line-height: 1.2em; 78 | font-weight: normal; 79 | } 80 | 81 | .content { 82 | padding: $picnic-separation; 83 | font-size: 90%; 84 | 85 | p { 86 | margin: 0 0 $picnic-separation; 87 | } 88 | 89 | h1, h2, h3, h4, h5, h6 { 90 | padding: 0.4em 0 0.1em; 91 | } 92 | h1 { 93 | font-size: 1.6em; 94 | } 95 | h2 { 96 | font-size: 1.45em; 97 | } 98 | h3 { 99 | font-size: 1.3em; 100 | } 101 | h4 { 102 | font-size: 1.15em; 103 | } 104 | h5 { 105 | font-size: 1.05em; 106 | } 107 | h6 { 108 | font-size: 0.95em; 109 | } 110 | } 111 | 112 | .sub-stack { 113 | @extend .stack; 114 | width: 90%; 115 | min-width: unset; 116 | margin: 0 0 0 auto; 117 | } 118 | 119 | .card { 120 | &.info { 121 | background: $picnic-info; 122 | color: $picnic-white; 123 | 124 | * { 125 | color: $picnic-white; 126 | } 127 | } 128 | &.success { 129 | background: $picnic-success; 130 | color: $picnic-white; 131 | 132 | * { 133 | color: $picnic-white; 134 | } 135 | } 136 | &.warning { 137 | background: $picnic-warning; 138 | color: $picnic-white; 139 | 140 | * { 141 | color: $picnic-white; 142 | } 143 | } 144 | &.error { 145 | background: $picnic-error; 146 | color: $picnic-white; 147 | 148 | * { 149 | color: $picnic-white; 150 | } 151 | } 152 | } 153 | 154 | .background-dull { 155 | background: $picnic-dull; 156 | } 157 | .background-light { 158 | background: $picnic-light; 159 | } 160 | 161 | th { 162 | background: $picnic-dull; 163 | * { 164 | color: $picnic-white; 165 | } 166 | } 167 | .light th { 168 | background: $picnic-light; 169 | color: $picnic-black; 170 | 171 | * { 172 | color: $picnic-black; 173 | } 174 | } 175 | 176 | // Handy Utilities 177 | .marginless { 178 | margin: 0 !important; 179 | } 180 | .paddingless { 181 | padding: 0 !important; 182 | } 183 | .inline { 184 | display: inline !important; 185 | } 186 | .italic { 187 | font-style: italic !important; 188 | } 189 | .large { 190 | font-size: 1.5em; 191 | } 192 | .small { 193 | font-size: 0.8em; 194 | } 195 | .left-align { 196 | text-align: left !important; 197 | } 198 | .right-align { 199 | text-align: right !important; 200 | } 201 | .center-align { 202 | text-align: center !important; 203 | } 204 | .clear { 205 | clear: both; 206 | } 207 | .pull-left { 208 | float: left; 209 | } 210 | .pull-right { 211 | float: right; 212 | } 213 | 214 | @media (max-width: 960px) { 215 | .menu ul li { 216 | display: block; 217 | } 218 | } 219 | 220 | @media (max-width: 599px) { 221 | .container { 222 | width: 98%; 223 | min-width: unset; 224 | } 225 | .brand { 226 | font-size: 75%; 227 | } 228 | } 229 | 230 | @media (min-width: 599px) { 231 | .left-align-desktop { 232 | text-align: left; 233 | } 234 | .right-align-desktop { 235 | text-align: right; 236 | } 237 | .center-align-desktop { 238 | text-align: center; 239 | } 240 | .pull-left-desktop { 241 | float: left; 242 | } 243 | .pull-right-desktop { 244 | float: right; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /app/styles/picnic-customizations/theme/_colors.scss: -------------------------------------------------------------------------------- 1 | // Color variables 2 | // - Cool 3 | // - Warm 4 | // - Gray Scale 5 | // 6 | // clrs, from https://github.com/mrmrs/colors 7 | 8 | // Cool 9 | $aqua: #7fdbff !default; 10 | $blue: #0074d9 !default; 11 | $navy: #001f3f !default; 12 | $teal: #39cccc !default; 13 | $green: #2ecc40 !default; 14 | $olive: #3d9970 !default; 15 | $lime: #01ff70 !default; 16 | 17 | // Warm 18 | $yellow: #ffdc00 !default; 19 | $orange: #ff851b !default; 20 | $red: #ff4136 !default; 21 | $fuchsia: #f012be !default; 22 | $purple: #b10dc9 !default; 23 | $maroon: #85144b !default; 24 | 25 | // Gray Scale 26 | $white: #fff !default; 27 | $silver: #ddd !default; 28 | $gray: #aaa !default; 29 | $black: #111 !default; 30 | -------------------------------------------------------------------------------- /app/styles/picnic-customizations/theme/_theme.scss: -------------------------------------------------------------------------------- 1 | // Top level variables for Picnic CSS 2 | // Note: some others are available under each specific plugin 3 | 4 | @import './colors'; 5 | 6 | 7 | // Colors (from /themes/default/colors) 8 | $picnic-white: $white !default; 9 | $picnic-black: $black !default; 10 | $picnic-primary: $teal !default; 11 | $picnic-success: $green !default; 12 | $picnic-info: $blue !default; 13 | $picnic-warning: $orange !default; 14 | $picnic-error: $red !default; 15 | $picnic-dull: $gray !default; 16 | $picnic-light: $silver !default; 17 | $picnic-color-variation: 10% !default; 18 | $picnic-transparency: .2 !default; 19 | 20 | 21 | // Spaces 22 | $picnic-separation: .6em !default; 23 | $picnic-breakpoint: 60em !default; 24 | 25 | 26 | // Shapes 27 | $picnic-radius: .2em !default; 28 | $picnic-border: 1px solid #ccc !default; 29 | $picnic-shadow: 0 0 .2em rgba($picnic-black, $picnic-transparency) !default; 30 | $picnic-overlay: rgba($picnic-black, $picnic-transparency); 31 | 32 | // Transitions 33 | $picnic-transition: all .3s; 34 | -------------------------------------------------------------------------------- /app/views/404.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const errorView = (state, emit, i18n) => { 4 | return html`
5 |
6 |

${i18n.__('404.header')}

7 |
8 |
9 |

${i18n.__('404.subheader')}

10 |
11 |
`; 12 | } 13 | 14 | module.exports = { errorView }; 15 | -------------------------------------------------------------------------------- /app/views/about.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const aboutView = (state, emit, i18n) => { 4 | const content = html`
`; 5 | const community = html`
`; 6 | 7 | const promises = []; 8 | if (typeof i18n.pages.about === 'undefined' || typeof i18n.pages.community === 'undefined') { 9 | promises.push(i18n.fetchLocalePage('about')); 10 | promises.push(i18n.fetchLocalePage('community')); 11 | } else { 12 | content.innerHTML = i18n.pages.about; 13 | community.innerHTML = i18n.pages.community; 14 | } 15 | if (promises.length > 0) { 16 | Promise.all(promises).then(fulfilled => emit(state.events.RENDER)); 17 | } 18 | return [ 19 | content, 20 | community, 21 | ]; 22 | } 23 | 24 | module.exports = { aboutView }; 25 | -------------------------------------------------------------------------------- /app/views/controller.js: -------------------------------------------------------------------------------- 1 | class ViewController { 2 | constructor(state, i18n, viewName, defaultState = {}) { 3 | // Store the global app state so it's accessible but out of the way. 4 | this.appState = state; 5 | this.i18n = i18n; 6 | this.i18n.__ = this.i18n.__.bind(i18n); // Allow pulling out just the `__` function for shortened translation declaration. 7 | 8 | // Give this view its own state within the appState. 9 | if (!this.appState.viewStates.hasOwnProperty(viewName)) { 10 | this.appState.viewStates[viewName] = defaultState; 11 | } 12 | this.state = this.appState.viewStates[viewName]; 13 | } 14 | 15 | get isLoggedIn () { 16 | return this.appState.isLoggedIn; 17 | } 18 | } 19 | 20 | module.exports = { ViewController }; 21 | -------------------------------------------------------------------------------- /app/views/global.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | let headerImage; 4 | 5 | if (typeof window !== 'undefined') { 6 | // Make Parcel bundler process image 7 | headerImage = require('../../dev/images/header.png'); 8 | } else { 9 | // Make server get processed image path 10 | const fs = require('fs'); 11 | const path = require('path'); 12 | const publicPath = path.resolve('public'); 13 | const publicFiles = fs.readdirSync(publicPath); 14 | const headerImageFileName = publicFiles.find(fileName => /header\..+?\.png/.test(fileName)); 15 | headerImage = path.relative(publicPath, path.resolve(publicPath, headerImageFileName)); 16 | } 17 | 18 | const globalView = (state, emit, view) => { 19 | const { i18n } = state; 20 | if (state.isFrontend && i18n.needsFetch) { 21 | return html``; 22 | } 23 | // Create a wrapper for view content that includes global header/footer 24 | return html` 25 |
26 | 53 |
54 | 55 |
56 | ${view(state, emit, i18n)} 57 |
58 | 59 | 85 | `; 86 | } 87 | 88 | module.exports = { globalView }; 89 | -------------------------------------------------------------------------------- /app/views/home/controller.js: -------------------------------------------------------------------------------- 1 | const { ViewController } = require('../controller'); 2 | 3 | class HomeController extends ViewController { 4 | constructor(state, i18n) { 5 | // Super passes state, view name, and default state to ViewController, 6 | // which stores state in this.appState and the view controller's state to this.state 7 | super(state, i18n, 'home', { 8 | loggedOut: { 9 | recentReviews: [], 10 | recentUpdates: [], 11 | }, 12 | loggedIn: { 13 | readingShelfId: null, 14 | updates: [], // statuses, ratings, and reviews from people you follow. 15 | interactions: [], // likes, comments, recommendations, etc. 16 | }, 17 | }); 18 | 19 | // If using controller methods in an input's onchange or onclick instance, 20 | // either bind the class's 'this' instance to the method first... 21 | // or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead. 22 | } 23 | } 24 | 25 | module.exports = { HomeController } 26 | -------------------------------------------------------------------------------- /app/views/home/index.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const { HomeController } = require('./controller'); // The controller for this view, where processing should happen. 4 | const { loggedOutView } = require('./loggedOut'); 5 | const { loggedInView } = require('./loggedIn'); 6 | 7 | // This is the view function that is exported and used in the view manager. 8 | const homeView = (state, emit, i18n) => { 9 | const controller = new HomeController(state, i18n); 10 | 11 | // Returning an array in a view allows non-shared parent HTML elements. 12 | // This one doesn't have the problem right now, but it's good to remember. 13 | return [ 14 | (!controller.isLoggedIn 15 | ? loggedOutView(controller, emit) 16 | : loggedInView(controller, emit) 17 | ), 18 | ]; 19 | } 20 | 21 | module.exports = { homeView }; 22 | -------------------------------------------------------------------------------- /app/views/home/loggedIn.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const { ShelvesController } = require('../shelves/controller'); 4 | 5 | const loggedInView = (homeController, emit) => { 6 | const { __ } = homeController.i18n; 7 | 8 | const shelvesController = new ShelvesController(homeController.appState, homeController.appState.i18n); 9 | 10 | const { readingShelfId } = homeController.state; 11 | const readingShelf = readingShelfId && typeof shelvesController.state.loadedShelves[readingShelfId] !== 'undefined' 12 | ? shelvesController.state.loadedShelves[readingShelfId] 13 | : null; 14 | console.log(readingShelf); 15 | 16 | if (shelvesController.appState.isFrontend && shelvesController.state.myShelves.length <= 0) { 17 | shelvesController.getUserShelves().then(() => { 18 | const readingShelfId = shelvesController.state.myShelves.find(shelf => shelf.name === 'Reading').id; 19 | console.log(readingShelfId); 20 | homeController.state.readingShelfId = readingShelfId + '/'; 21 | console.log(homeController.state); 22 | return shelvesController.getShelf(homeController.state.readingShelfId); 23 | }).then(() => { 24 | emit(shelvesController.appState.events.RENDER); 25 | }); 26 | } 27 | 28 | return [ 29 | html`
30 |

${__('home.logged_in.subtitle')}

31 |
32 |

Reading

33 |
34 | ${ 35 | readingShelf === null 36 | ? html`` 37 | : readingShelf.shelfItems.map((shelfItem, shelfItemIndex) => { 38 | return html`
39 | ${ shelfItem.title } 40 |
`; 41 | }) 42 | } 43 |
44 |
45 |
46 |
47 |
48 |
49 |

${__('home.logged_in.updates')}

50 | 53 |
54 |
55 | ${homeController.state.loggedIn.updates.map(update => reviewCard(homeController, update))} 56 |
57 |
58 |
59 |
60 |
61 |
62 |

${__('home.logged_in.interactions')}

63 | 66 |
67 |
68 | ${homeController.state.loggedIn.interactions.map(interaction => reviewCard(homeController, interaction))} 69 |
70 |
71 |
72 |
73 |
`, 74 | ]; 75 | } 76 | 77 | module.exports = { loggedInView }; 78 | -------------------------------------------------------------------------------- /app/views/home/loggedOut.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const loggedOutView = (homeController, emit) => { 4 | const { __ } = homeController.i18n; 5 | 6 | return [ 7 | html`
8 |

${__('home.logged_out.subtitle')}

9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | ${__('home.logged_out.track_books')} 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 | ${__('home.logged_out.share_friends')} 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | ${__('home.logged_out.read_rate')} 43 |
44 |
45 |
46 |
47 |
`, 48 | html`
49 |

${__('home.logged_out.community_header')}

50 |
51 |
52 |
53 |
54 |

${__('home.logged_out.recent_reviews')}

55 | 58 |
59 |
60 | ${homeController.state.loggedOut.recentReviews.map(review => reviewCard(homeController, review))} 61 |
62 |
63 |
64 |
65 |
66 |
67 |

${__('home.logged_out.recent_updates')}

68 | 71 |
72 |
73 | ${homeController.state.loggedOut.recentUpdates.map(update => reviewCard(homeController, update))} 74 |
75 |
76 |
77 |
78 |
`, 79 | html`
80 | ${__('home.logged_out.join_now')} 81 |
`, 82 | ]; 83 | } 84 | 85 | module.exports = { loggedOutView }; 86 | -------------------------------------------------------------------------------- /app/views/login/controller.js: -------------------------------------------------------------------------------- 1 | const { ViewController } = require('../controller'); 2 | 3 | class LoginController extends ViewController { 4 | constructor(state, emit, i18n) { 5 | // Super passes state, view name, and default state to ViewController, 6 | // which stores state in this.appState and the view controller's state to this.state 7 | super(state, i18n, 'login', { 8 | fieldValues: { 9 | loginEmail: '', 10 | loginPassword: '', 11 | createEmail: '', 12 | createUsername: '', 13 | createDisplayName: '', 14 | createPassword: '', 15 | createConfirm: '', 16 | createPermission: 100, 17 | }, 18 | loginError: '', 19 | createError: '', 20 | loginMessage: '', 21 | createMessage: '', 22 | pageMessage: '', 23 | isChecking: false, 24 | }); 25 | 26 | this.emit = emit; 27 | 28 | // If using controller methods in an input's onchange or onclick instance, 29 | // either bind the class's 'this' instance to the method first... 30 | // or use `onclick=${() => controller.submit()}` to maintain the 'this' of the class instead. 31 | } 32 | 33 | clearLoginForm () { 34 | this.state.fieldValues.loginEmail = ''; 35 | this.state.fieldValues.loginPassword = ''; 36 | 37 | this.emit(this.appState.events.RENDER); 38 | } 39 | 40 | clearCreateAccountForm () { 41 | this.state.fieldValues.createEmail = ''; 42 | this.state.fieldValues.createUsername = ''; 43 | this.state.fieldValues.createDisplayName = ''; 44 | this.state.fieldValues.createPassword = ''; 45 | this.state.fieldValues.createConfirm = ''; 46 | 47 | this.emit(this.appState.events.RENDER); 48 | } 49 | 50 | validateLogin () { 51 | const { __ } = this.i18n; 52 | this.state.loginError = ''; 53 | this.state.isChecking = true; 54 | 55 | this.emit(this.appState.events.RENDER, () => { 56 | const { 57 | loginEmail, 58 | loginPassword 59 | } = this.state.fieldValues; 60 | 61 | if ([ 62 | loginEmail, 63 | loginPassword, 64 | ].includes('')) { 65 | this.state.loginError = __('login.login_required_field_blank'); 66 | this.state.isChecking = false; 67 | this.emit(this.appState.events.RENDER); 68 | return; 69 | } 70 | 71 | this.logIn(); 72 | }); 73 | } 74 | 75 | validateCreateAccount () { 76 | const { __ } = this.i18n; 77 | this.state.createError = ''; 78 | this.state.isChecking = true; 79 | 80 | this.emit(this.appState.events.RENDER, () => { 81 | const { 82 | createEmail, 83 | createUsername, 84 | createPassword, 85 | createConfirm 86 | } = this.state.fieldValues; 87 | 88 | if ([ 89 | createEmail, 90 | createUsername, 91 | createPassword, 92 | createConfirm, 93 | ].includes('')) { 94 | this.state.createError = __('login.create_required_field_blank'); 95 | this.state.isChecking = false; 96 | this.emit(this.appState.events.RENDER); 97 | return; 98 | } 99 | 100 | if (createPassword !== createConfirm) { 101 | this.state.createError = __('login.create_password_confirm_mismatch'); 102 | this.state.isChecking = false; 103 | this.emit(this.appState.events.RENDER); 104 | return; 105 | } 106 | 107 | this.createAccount(); 108 | }); 109 | } 110 | 111 | logIn () { 112 | const { __ } = this.i18n; 113 | const { 114 | loginEmail, 115 | loginPassword 116 | } = this.state.fieldValues; 117 | 118 | fetch('/api/account/login', { 119 | method: 'post', 120 | headers: { 121 | 'Content-Type': 'application/json', 122 | }, 123 | body: JSON.stringify({ 124 | email: loginEmail, 125 | password: loginPassword, 126 | }), 127 | }).then(response => response.json()) 128 | .then(response => { 129 | if (response.error !== false) { 130 | console.error(response); 131 | this.state.loginError = __(response.message); 132 | this.state.isChecking = false; 133 | this.emit(this.appState.events.RENDER); 134 | return; 135 | } 136 | 137 | this.appState.isLoggedIn = true; 138 | this.state.loginMessage = __(response.message); 139 | this.state.isChecking = false; 140 | this.clearLoginForm(); 141 | }) 142 | } 143 | 144 | createAccount () { 145 | const { __ } = this.i18n; 146 | const { 147 | createEmail, 148 | createUsername, 149 | createDisplayName, 150 | createPassword, 151 | createPermission 152 | } = this.state.fieldValues; 153 | 154 | fetch('/api/account/create', { 155 | method: 'post', 156 | headers: { 157 | 'Content-Type': 'application/json', 158 | }, 159 | body: JSON.stringify({ 160 | email: createEmail, 161 | username: createUsername, 162 | displayName: createDisplayName, 163 | password: createPassword, 164 | permissionLevel: createPermission, 165 | }), 166 | }).then(response => response.json()) 167 | .then(response => { 168 | if (response.error !== false) { 169 | console.error(response); 170 | this.state.createError = __(response.message); 171 | this.state.isChecking = false; 172 | this.emit(this.appState.events.RENDER); 173 | return; 174 | } 175 | 176 | this.state.createMessage = __(response.message); 177 | this.state.isChecking = false; 178 | this.clearCreateAccountForm(); 179 | }) 180 | } 181 | } 182 | 183 | module.exports = { LoginController }; 184 | -------------------------------------------------------------------------------- /app/views/login/index.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const { LoginController } = require('./controller'); 4 | 5 | const loginView = (state, emit, i18n) => { 6 | const controller = new LoginController(state, emit, i18n); 7 | const { __ } = controller.i18n; 8 | 9 | if (controller.appState.isLoggedIn === true) { 10 | setTimeout(() => { 11 | controller.state.loginMessage = ''; 12 | emit('pushState', '/') 13 | }, 3000); 14 | 15 | return html``; 29 | } 30 | 31 | return html`
32 | 33 | ${ 34 | controller.state.pageMessage === '' 35 | ? null 36 | : html`
${controller.state.pageMessage}
` 37 | } 38 | 39 |
40 |
41 |
42 |
43 |

${__('login.log_in')}

44 |
45 |
46 | 54 | 62 | ${ 63 | controller.state.loginError === '' 64 | ? null 65 | : html`
${controller.state.loginError}
` 66 | } 67 | 76 |
77 |
78 |
79 |
80 |
81 |
82 |

${__('login.create_account')}

83 |
84 |
85 | ${ 86 | controller.state.createMessage === '' 87 | ? null 88 | : html`
${controller.state.createMessage}
` 89 | } 90 | 98 | 106 | 114 | 122 | 130 | 146 | ${ 147 | controller.state.createError === '' 148 | ? null 149 | : html`
${controller.state.createError}
` 150 | } 151 | 161 |
162 |
163 |
164 |
165 | 166 |
`; 167 | } 168 | 169 | module.exports = { loginView }; 170 | -------------------------------------------------------------------------------- /app/views/partials/modal.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html'); 2 | 3 | const modal = (modalId, controller, contentHTML, options = {}) => { 4 | /* Options: 5 | * controller : Pass the controller class with state; Requires get/set for openModal in state. 6 | * buttonHTML : Displayed in place of the default button to open the modal 7 | * buttonText : Displayed if no buttonHTML is specified 8 | * buttonClasses : Used with buttonText. If excluded, 'button' is used. 9 | * noHeader : Set to `true` and exclude headerHTML to not include a modal header 10 | * headerHTML : Displayed in place of the default header; Recommended to use `
` tag 11 | * headerText : Displayed in an `

` if no header is specified 12 | * noFooter : Set to `true` and exclude footerHTML to not include a modal footer 13 | * footerHTML : Displayed in place of the default footer; Recommended to use `