Skip to content

Commit

Permalink
Merge pull request #14 from buzcarter/feature/keep_most_recent_views_…
Browse files Browse the repository at this point in the history
…and_searches

Feature/keep most recent views and searches
  • Loading branch information
buzcarter authored Dec 11, 2023
2 parents cc767fd + 922ebad commit f819588
Show file tree
Hide file tree
Showing 21 changed files with 413 additions and 40 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ module.exports = {
// 'jest/globals': true,
// },
globals: {
beforeEach: 'readonly',
describe: 'readonly',
it: 'readonly',
expect: 'readonly',
it: 'readonly',
},
}],
};
1 change: 1 addition & 0 deletions .jest/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
},
},
rootDir: resolve(__dirname, '../'),
setupFiles: ['<rootDir>/.jest/jest.setup.cjs'],
setupFilesAfterEnv: [],
testMatch: [
'<rootDir>/src/**/__tests__/**/*.test.js',
Expand Down
1 change: 1 addition & 0 deletions .jest/jest.setup.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require( 'jest-localstorage-mock');
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "recipes-nodejs",
"version": "1.0.3",
"version": "1.0.4",
"description": "NodeJS recipe publisher, converts markdown/text recipe files into well formatted HTML.",
"main": "build.js",
"type": "module",
Expand Down Expand Up @@ -32,6 +32,7 @@
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.0",
"jest": "^29.7.0",
"jest-localstorage-mock": "^2.4.26",
"nodemon": "^3.0.1"
},
"repository": {
Expand Down
10 changes: 5 additions & 5 deletions src/buildRecipes.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ const RegExes = Object.freeze({

const LINK_SUB_NAME = '<name>';

function setHeadMeta(documentHtml, { favicon, ogImgURL, recipeName, titleSuffix }) {
function setHeadMeta(documentHtml, { author, favicon, ogImgURL, recipeName, titleSuffix }) {
return documentHtml
.replace(RegExes.PAGE_TITLE, `<title>${recipeName}${titleSuffix || ''}</title>`)
.replace(RegExes.PAGE_TITLE, `<title>${recipeName}${author ? ` by ${author}` : ''}${titleSuffix || ''}</title>`)
.replace(Substitutions.META_DATE, `<meta name="date" content="${new Date()}">`)
.replace(Substitutions.META_OG_IMG, ogImgURL ? `<meta property="og:image" content="${ogImgURL}">` : '')
.replace(Substitutions.META_FAVICON, favicon ? `<link rel="icon" type="image/png" href="${favicon}">` : '')
Expand Down Expand Up @@ -175,7 +175,7 @@ function getHelpSection(helpURLs, name) {

function convertRecipe(outputHTML, recipeHTML, opts) {
const {
name,
author, name,
heroImgURL: image,
autoUrlSections, defaultTheme, favicon, useFractionSymbols, helpURLs, includeHelpLinks, shortenURLs, titleSuffix,
} = opts;
Expand Down Expand Up @@ -227,7 +227,7 @@ function convertRecipe(outputHTML, recipeHTML, opts) {

outputHTML = sectionMgr.replace(outputHTML);

return setHeadMeta(outputHTML, { favicon, ogImgURL: heroImgURL, recipeName, titleSuffix })
return setHeadMeta(outputHTML, { author, favicon, ogImgURL: heroImgURL, recipeName, titleSuffix })
.replace(Substitutions.HELP, showHelp ? getHelpSection(helpURLs, name) : '')
.replace(Substitutions.HERO_IMG, heroImgURL ? `<a class=${Styles.HERO_IMG_LINK} href="${heroImgURL}" target="_blank"><img class=${Styles.HERO_IMG} src="${heroImgURL}"></a>` : '')
.replace(Substitutions.THEME_CSS, `theme-${customizations.style || defaultTheme}`)
Expand Down Expand Up @@ -267,7 +267,7 @@ export default function buildRecipes(recipeTemplate, options, fileList, images)
if (addImageLinks) {
html = linkifyImages(html);
}
html = prettyHtml(convertRecipe(recipeTemplate, html, { ...options, name, heroImgURL }), { ocd: true });
html = prettyHtml(convertRecipe(recipeTemplate, html, { ...options, author, heroImgURL, name }), { ocd: true });
writeFile(resolve(outputPath, `${name}.html`), html, { encoding: 'utf8' }, () => {
if (fileList.length === ++fileCount) {
promResolve(recipeInfo);
Expand Down
2 changes: 1 addition & 1 deletion src/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const RegExes = {
// #endregion

// #region Find Author Credit
AUTHOR: /^(?:#{3,6})?\s*(?:recipe )?(?:adapted by|author|by|courtesy(?: of)?|from(?: the)? kitchen of|from|source)\s*[ :-]\s*([A-Z][\w '"]+)/im,
AUTHOR: /^(?:#{3,6})?\s*(?:recipe )?(?:adapted (?:by|from)|author|by|courtesy(?: of)?|from(?: the)? kitchen of|from|source)\s*[ :-]\s*([A-Z][\w '"]+)/im,
// #endregion
};

Expand Down
6 changes: 6 additions & 0 deletions src/static/images/icons/recents.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/static/scripts/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { init as initViewBtns } from './libs/viewPicker.js';
import { init as initRecentlyViewed } from './libs/recentlyViewed.js';
import { init as initSearchBox } from './libs/searchBox.js';
import { init as initViewBtns } from './libs/viewPicker.js';
import { KeyNames, getKey } from './libs/preferences.js';

function run() {
Expand All @@ -8,6 +9,7 @@ function run() {

initSearchBox(search);
initViewBtns(view);
initRecentlyViewed();
}

run();
97 changes: 97 additions & 0 deletions src/static/scripts/libs/__tests__/preferences.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
KeyNames,
updateKey,
getKey,
updateMRUList,
} from '../preferences';

describe('preferences', () => {
beforeEach(() => {
localStorage.clear();
});

describe('updateKey', () => {
it('should update the value of a key in the app data', () => {
const key = KeyNames.RECENT;
const value = ['recipe1', 'recipe2'];

updateKey(key, value);

const updatedValue = getKey(key);
expect(updatedValue).toEqual(value);
});
});

describe('getKey', () => {
it('should return the default value when `key` has not been set', () => {
const key = KeyNames.SEARCH;
const defaultValue = 'default';

const value = getKey(key, defaultValue);
expect(value).toEqual(defaultValue);
});

it('should return the default value if the key does not exist', () => {
const key = 'nonExistentKey';
const defaultValue = 'default';

const value = getKey(key, defaultValue);
expect(value).toEqual(defaultValue);
});
});

describe('updateMRUList', () => {
it('should add a value to the MRU list', () => {
const key = KeyNames.RECENT;
const maxLength = 3;
const value = 'recipe1';

updateMRUList(key, maxLength, value);

const updatedList = getKey(key);
expect(updatedList).toEqual([value]);
});

it('should place new values at top of the MRU list', () => {
const key = KeyNames.RECENT;
const maxLength = 3;
const value1 = 'recipe1';
const value2 = 'recipe2';

updateMRUList(key, maxLength, value1);
updateMRUList(key, maxLength, value2);

const updatedList = getKey(key);
expect(updatedList).toEqual([value2, value1]);
});

it('should move an existing value to the top of the MRU list', () => {
const key = KeyNames.RECENT;
const maxLength = 3;
const value1 = 'recipe1';
const value2 = 'recipe2';

updateMRUList(key, maxLength, value1);
updateMRUList(key, maxLength, value2);
updateMRUList(key, maxLength, value1);

const updatedList = getKey(key);
expect(updatedList).toEqual([value1, value2]);
});

it('should discard the last item if the MRU list exceeds the max length', () => {
const key = KeyNames.RECENT;
const maxLength = 2;
const value1 = 'recipe1';
const value2 = 'recipe2';
const value3 = 'recipe3';

updateMRUList(key, maxLength, value1);
updateMRUList(key, maxLength, value2);
updateMRUList(key, maxLength, value3);

const updatedList = getKey(key);
expect(updatedList).toEqual([value3, value2]);
});
});
});
20 changes: 20 additions & 0 deletions src/static/scripts/libs/__tests__/recentlyViewed.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { toDisplay } from '../recentlyViewed';

describe('recentlyViewed', () => {
describe('toDisplay', () => {
const tests = [
{ value: 'hello-world', expectedResult: 'Hello World' },
{ value: 'hello world', expectedResult: 'Hello World' },
{ value: ' HeLLo WoRLD ', expectedResult: 'Hello World' },
{ value: '', expectedResult: '' },
{ value: '---', expectedResult: '' },
];

tests.forEach((test) => {
it(`should convert "${test.value}" to "${test.expectedResult}"`, () => {
const result = toDisplay(test.value);
expect(result).toEqual(test.expectedResult);
});
});
});
});
21 changes: 21 additions & 0 deletions src/static/scripts/libs/__tests__/searchBox.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { scrub } from '../searchBox';

describe('searchBox', () => {
describe('scrub', () => {
const tests = [
{ value: 'Hello World!', expectedResult: 'helloworld' },
{ value: ' A very delicious pumpkin -- pie! From, :Hasbro (99) ', expectedResult: 'averydeliciouspumpkinpiefromhasbro99' },
{ value: ' Hello World ', expectedResult: 'helloworld' },
{ value: '12345', expectedResult: '12345' },
{ value: '', expectedResult: '' },
{ value: '---', expectedResult: '' },
];

tests.forEach((test) => {
it(`should scrub "${test.value}" to "${test.expectedResult}"`, () => {
const result = scrub(test.value);
expect(result).toEqual(test.expectedResult);
});
});
});
});
32 changes: 31 additions & 1 deletion src/static/scripts/libs/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ const StorageKeys = {
};

export const KeyNames = Object.freeze({
VIEW: 'content',
/** most recently viewed recipe list */
RECENT: 'recent',
/** last search term */
SEARCH: 'search',
/** most recent search term list */
SEARCH_HISTORY: 'searchHistory',
/** most recently selected view/layout */
VIEW: 'content',
});

const setAppData = (payload) => localStorage.setItem(StorageKeys.APP_NAME, JSON.stringify(payload));
Expand Down Expand Up @@ -34,3 +40,27 @@ export const getKey = (key, defaultValue) => {
const value = payload[key];
return value === undefined ? defaultValue : value;
};

/**
* Add `value` in first position of list named `key`. When list length
* exceeds `maxLength` last item is discarded.
*/
export const updateMRUList = (key, maxLength, value) => {
let list = getKey(key, []);
const index = list.indexOf(value);
if (index === 0) {
return;
}

if (index > -1) {
list.splice(index, 1);
}

list.unshift(value);

if (list.length > maxLength) {
list = list.slice(0, maxLength);
}

updateKey(key, list);
};
58 changes: 58 additions & 0 deletions src/static/scripts/libs/recentlyViewed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { KeyNames, getKey, updateMRUList } from './preferences.js';

const MAX_LIST_LENGTH = 50;

/* eslint-disable key-spacing */
const Selectors = {
RECIPE_LIST: '#recipe-list',

RECENTLY_VIEWED_BTN: '#show-recents-btn',

MODAL: '#show-recents-modal',
MODAL_CONTENT: '#show-recents-content',
MODAL_CLOST_BTN: '#show-recents-close-btn',
};
/* eslint-enable key-spacing */

const Styles = {
MODAL_ACTIVE: 'modal--is-active',
};

export const toDisplay = (value) => value
.replace(/-/g, ' ')
.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
.replace(/\s+/g, ' ')
.trim();

function onRecipeLinkClick(e) {
const anchor = e.target.tagName === 'A'
? e.target
: e.target.closest('a');

if (anchor) {
const value = anchor.getAttribute('href');
if (value) {
updateMRUList(KeyNames.RECENT, MAX_LIST_LENGTH, value.replace(/^.*\/|\.html$/g, ''));
}
}
}

function toggleModal(isActive) {
const modal = document.querySelector(Selectors.MODAL);
modal.classList.toggle(Styles.MODAL_ACTIVE, isActive);
modal.setAttribute('aria-hidden', !isActive);
}

function onViewBtnClick() {
toggleModal(true);
document.querySelector(Selectors.MODAL_CONTENT).innerHTML = getKey(KeyNames.RECENT)
.map((value) => `<li><a href="${value}.html">${toDisplay(value)}</a></li>`)
.join('');
}

export function init() {
document.querySelector(Selectors.RECENTLY_VIEWED_BTN).addEventListener('click', onViewBtnClick);
// event delegation: record all recipe link clicks
document.querySelector(Selectors.RECIPE_LIST).addEventListener('click', onRecipeLinkClick);
document.querySelector(Selectors.MODAL_CLOST_BTN).addEventListener('click', () => toggleModal(false));
}
Loading

0 comments on commit f819588

Please sign in to comment.