{"id":32331692,"url":"https://github.com/chrisnajman/filter-quotes-v2","last_synced_at":"2026-04-15T15:31:47.553Z","repository":{"id":319253243,"uuid":"1073738321","full_name":"chrisnajman/filter-quotes-v2","owner":"chrisnajman","description":"A lightweight app for displaying a collection of quotations from a JSON file using an HTML template. Short quotes use \u003cq\u003e, longer ones use \u003cblockquote\u003e, and entries can be filtered by author or tag.","archived":false,"fork":false,"pushed_at":"2025-10-17T08:25:00.000Z","size":132,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-18T11:38:51.272Z","etag":null,"topics":["accessibility","aria-attributes","css-flexbox","css-grid","css-imports","css-nesting","cssnano","details","es6-modules","esbuild","html-css-javascript","html-minifier-terser","html-template","json","loading-spinner","no-js","postcss","resize-observer","sticky-headers","theme-switcher"],"latest_commit_sha":null,"homepage":"https://chrisnajman.github.io/filter-quotes-v2/","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisnajman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-10T14:45:12.000Z","updated_at":"2025-10-17T08:25:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chrisnajman/filter-quotes-v2","commit_stats":null,"previous_names":["chrisnajman/filter-quotes-v2"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/chrisnajman/filter-quotes-v2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisnajman%2Ffilter-quotes-v2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisnajman%2Ffilter-quotes-v2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisnajman%2Ffilter-quotes-v2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisnajman%2Ffilter-quotes-v2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisnajman","download_url":"https://codeload.github.com/chrisnajman/filter-quotes-v2/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisnajman%2Ffilter-quotes-v2/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280683814,"owners_count":26372970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-23T02:00:06.710Z","response_time":142,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accessibility","aria-attributes","css-flexbox","css-grid","css-imports","css-nesting","cssnano","details","es6-modules","esbuild","html-css-javascript","html-minifier-terser","html-template","json","loading-spinner","no-js","postcss","resize-observer","sticky-headers","theme-switcher"],"created_at":"2025-10-23T19:54:21.250Z","updated_at":"2026-04-15T15:31:47.546Z","avatar_url":"https://github.com/chrisnajman.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Filter Quotations\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong id=\"menu\"\u003eMenu\u003c/strong\u003e\u003c/summary\u003e\n\n- [Description](#description)\n- [Features](#features)\n- [JSON](#json)\n- [HTML `\u003ctemplate\u003e`s](#html-templates)\n- [JavaScript](#javascript)\n- [No JS](#no-js)\n- [Accessibility](#accessibility)\n- [Theme Toggling](#theme-toggling)\n- [Testing and Compatibility](#testing-and-compatibility)\n- [How to Run](#how-to-run)\n- [Build \u0026 Deployment Setup for `/docs` Folder](#build--deployment-setup-for-docs-folder)\n\n\u003c/details\u003e\n\n## Description\n\nA lightweight web app for browsing and filtering a collection of quotations by author or tag. Built with semantic HTML and modern vanilla JavaScript, it uses a JSON data source and an HTML `\u003ctemplate\u003e` to generate the quotation list dynamically, with a focus on accessibility and clear structure.\n\nLonger entries are rendered as `\u003cblockquote\u003e` elements, while shorter ones use inline `\u003cq\u003e` tags for natural quotation formatting.\n\n- Includes an \"All Authors and Tags\" sidebar, listing every unique author and tag found in the JSON file.\n- Clicking any author or tag in the sidebar filters the quotations list using the same interactive logic as the buttons within individual quotations.\n- Sidebar panels close automatically when a selection is made.\n\n[View on GitHub Pages](https://chrisnajman.github.io/filter-quotations-v2)\n\n[Back to menu](#menu)\n\n---\n\n## Features\n\n### Filtering quotations\n\n- Displays a list of quotations, each containing one or more filter buttons (e.g. author names or tags).\n- Clicking an **active** filter button shows only the quotations that include that author or tag.\n- The active filter is visually highlighted across all matching buttons.\n- A \"Clear filters\" button resets the list to show all quotations.\n\n### Conditional rendering\n\n- Quotations of **25 words or fewer** are displayed using a `\u003cq\u003e` element (inline quotations).\n- Quotations **over 25 words** are displayed using a `\u003cblockquote\u003e` element (block-level quotations).\n- This enhances readability while maintaining semantic meaning.\n\n### All Authors and Tags Sidebar\n\n- Displays all authors and tags in a collapsible `\u003cdetails\u003e` panel on the side.\n- Buttons are generated dynamically from the JSON data.\n- Authors or tags that appear only once are **disabled** to indicate they cannot filter multiple quotations.\n- Clicking a sidebar button applies the same filter logic as quotation buttons and closes the panel automatically.\n- Buttons visually indicate the active filter.\n\n### Details dropdown\n\n- Keyboard support: pressing `Escape` closes the currently focused `\u003cdetails\u003e`.\n- ARIA attributes (`aria-expanded`) synchronized with open/close state.\n\n[Back to menu](#menu)\n\n---\n\n## JSON\n\nThe structure of a JSON entry is as follows:\n\n```json\n{\n  \"prequote\": \"Pre-quote\",\n  \"quote\": \"Quote\",\n  \"postquote\": \"Pre-quote\",\n  \"author\": \"Author\",\n  \"tags\": [\"Tag-1\", \"Tag-2\", \"Tag-3\", \"Tag-4\", \"Tag-5\", \"Tag-(n)\"]\n}\n```\n\nTo create your own quotations list, open `./json/quotes.json`, remove the existing entries, and add your own, using the template above as a model.\n\nDon't forget to separate entries with a comma, i.e.\n\n```json\n[\n  {\n    \"prequote\": \"\",\n    \"quote\": \"\",\n    \"postquote\": \"\",\n    \"author\": \"\",\n    \"tags\": []\n  },\n  {\n    \"prequote\": \"\",\n    \"quote\": \"\",\n    \"postquote\": \"\",\n    \"author\": \"\",\n    \"tags\": []\n  },\n  {\n    \"prequote\": \"\",\n    \"quote\": \"\",\n    \"postquote\": \"\",\n    \"author\": \"\",\n    \"tags\": []\n  }\n]\n```\n\n### Notes\n\n- The last entry in a JSON file is **not** followed by a comma.\n- Both **prequote** and **postquote** are optional. They can be used to supply a preamble or a postamble (real word!) to the quotation. If either are not required, leave the field value as `\"\"` and the corresponding HTML will be hidden.\n- If the **author** is unknown, leave the field value as `\"\"`: a button with a label of \"Unknown\" will be created.\n- You can create as many **tags** as you like. If you don't want any, leave the field value as an empty array (`[]`) and the entire list item will be hidden.\n\n[Back to menu](#menu)\n\n---\n\n## HTML `\u003ctemplate\u003e`s\n\nThe JSON entries are inserted into HTML `\u003ctemplate\u003e`s via `quotes-display.js`.\n\nThe `\u003ctemplate\u003e` element provides a reusable structure for each quotation, allowing the script to clone and populate it with data from the JSON file without repeating markup in the HTML.\n\n- The **All Authors and Tags** templates are populated dynamically from the JSON using `all-authors-display.js` and `all-tags-display.js`.\n- Unique authors and tags are converted into `\u003cbutton\u003e` elements and inserted into their respective `\u003ctemplate\u003e` containers.\n- Buttons that appear only once are automatically disabled, and all buttons are sorted alphabetically with enabled buttons displayed first.\n\n### Quotations `\u003ctemplate\u003e`\n\n```html\n\u003ctemplate id=\"quote-template\"\u003e\n  \u003cli class=\"quote-container quote flow\"\u003e\n    \u003c!-- Begin Quotations output --\u003e\n    \u003cdiv class=\"text-container\"\u003e\u003c/div\u003e\n    \u003c!-- End Quotations output --\u003e\n    \u003cul\u003e\n      \u003cli\u003e\n        \u003cspan class=\"info\"\u003eAuthor:\u003c/span\u003e\n        \u003cbutton\n          type=\"button\"\n          data-author\n        \u003e\u003c/button\u003e\n      \u003c/li\u003e\n      \u003cli\n        class=\"tags\"\n        id=\"tags\"\n      \u003e\n        \u003cspan class=\"info\"\u003eTags:\u003c/span\u003e\n        \u003cul\u003e\n          \u003cli\u003e\n            \u003cdiv\n              class=\"buttons-container\"\n              data-tags\n            \u003e\u003c/div\u003e\n          \u003c/li\u003e\n        \u003c/ul\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/li\u003e\n\u003c/template\u003e\n```\n\n### All Authors and Tags `\u003ctemplate\u003e`s\n\n```html\n\u003ctemplate id=\"all-authors-template\"\u003e\n  \u003cdiv\n    class=\"buttons\"\n    data-all-authors\n  \u003e\u003c/div\u003e\n\u003c/template\u003e\n\u003ctemplate id=\"all-tags-template\"\u003e\n  \u003cdiv\n    class=\"buttons\"\n    data-all-tags\n  \u003e\u003c/div\u003e\n\u003c/template\u003e\n```\n\n[Back to menu](#menu)\n\n---\n\n## JavaScript\n\nBuilt with **vanilla ES6 JavaScript**, focusing on modern syntax and browser APIs.\n\nThe JavaScript has been split into separate modules, improving code modularity:\n\n- `index.js`: Main entry point for the app.\n\n  - Defines and immediately runs an asynchronous `init()` function that orchestrates fetching, rendering, and filtering quotations:\n\n    - **`await getQuotesData()`** — fetches the JSON data and renders the quotations, also populating the sidebar.\n    - **`quotesButtons()`** — initializes filtering logic on quotation buttons and returns helper functions `applyFilter` and `filterInactive`.\n    - **`asideButtonFilters({ applyFilter, filterInactive })`** — connects sidebar author/tag buttons to the same filtering system.\n\n  - The four auxiliary functions — `themeSwitcher()`, `loadingAnimation()`, `details()`, and `pageHeaderResizeObserver()` — are called beforehand to set up global behaviors.\n\n- `quotes-get-data.js`: Fetches the JSON quotations file, passes the data to` quotes-display.js` to render the list, runs `quotes-buttons-count.js` to disable singleton buttons, and populates the sidebar via - `all-authors-display.js`, `all-tags-display.js`, and `all-authors-tags-buttons.js`.\n- `quotes-display.js`: Clones the quotation template for each JSON entry, inserts quote text, pre/post-quote text, author, and tags. Short quotes use `\u003cq\u003e`; long quotes use `\u003cblockquote\u003e`. Handles missing authors/tags gracefully.\n- `quotes-buttons.js`: Adds interactive filtering to quotation buttons and the \"Clear filters\" button. Tracks active filters, updates visibility of quotations, and highlights active buttons. Exports `applyFilter` and `filterInactive` for reuse by sidebar buttons.\n- `quotes-buttons-count.js`: Counts occurrences of each author/tag button and disables buttons that appear only once.\n\n- `all-authors-display.js`: Populates the \"All Authors\" sidebar panel with unique authors from JSON.\n- `all-tags-display.js`: Populates the \"All Tags\" sidebar panel with unique tags from JSON.\n- `all-authors-tags-buttons.js`: Applies sorting and disables singleton buttons in the sidebar panels.\n- `all-authors-tags-button-filters.js`: Connects sidebar buttons to the filtering logic in `quotes-buttons.j`s. Clicking a sidebar button applies a filter and closes the panel.\n\n- `utils.js`: Contains helper functions for counting occurrences, disabling singleton buttons, sorting, creating buttons from arrays, and calculating word count for quote formatting.\n\n### Other\n\n- `page-header-resize-observer.js`: Observes the `.header` element and updates the root’s `scroll-padding-top` based on header size and the `--skip-link-gap` CSS variable (in `base.css`), ensuring skip links and anchor targets scroll into view with proper spacing below a fixed or sticky header.\n\n- `details.js`: Handles `\u003cdetails\u003e` accessibility and behavior.\n\n  - Syncs `aria-expanded` between `\u003csummary\u003e` and `\u003cdetails\u003e`.\n  - Supports closing via the `Escape` key and restores focus.\n\n- `loader.js`: See [Loader Git repository](https://github.com/chrisnajman/loader)\n\n- `theme.js`: Handles theme toggling (light/dark mode) and local storage management.\n\n[Back to menu](#menu)\n\n---\n\n## No JS\n\nIf JavaScript is disabled, a warning message is displayed to the user.\n\nThe `no-js` class provides a simple fallback mechanism that lets you adjust styles and messages when JavaScript is unavailable.\n\n### How it Works\n\n#### HTML\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\n  lang=\"en\"\n  class=\"no-js\"\n\u003e\n  \u003chead\u003e\n    \u003c!-- If JavaScript IS enabled, remove the 'no-js' class from \u003chtml\u003e tag --\u003e\n    \u003cscript\u003e\n      document.documentElement.classList.remove(\"no-js\")\n    \u003c/script\u003e\n\n    \u003c!-- Rest of \u003chead\u003e items --\u003e\n  \u003c/head\u003e\n\n  \u003cbody\u003e\n    \u003c!-- \u003cbody\u003e content --\u003e\n    \u003cnoscript class=\"container\"\u003e\n      \u003cp\u003e\n        JavaScript must be enabled for the quotations to display and the\n        filtering to work.\n      \u003c/p\u003e\n    \u003c/noscript\u003e\n    \u003c!-- \u003cbody\u003e content --\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n#### CSS\n\n`no-js.css`:\n\n```css\n.no-js {\n  \u0026 .theme-toggler,\n  \u0026 .details-container,\n  \u0026 .quotes-container .btn-clear-filters,\n  \u0026 .authors-tags,\n  \u0026 .loader,\n  \u0026 .loader::after {\n    display: none;\n  }\n}\n\nnoscript p {\n  width: fit-content;\n  margin-inline: auto;\n  border: 0.1875rem solid var(--warning);\n  background-color: var(--body-fg);\n  color: var(--el-bg);\n  padding: 1em 1.5em;\n  border-radius: 0.75rem;\n  text-align: center;\n  text-wrap: balance;\n}\n```\n\nIn general, add any elements you want hidden inside the `.no-js { ... }` block.\n\n[Back to menu](#menu)\n\n---\n\n## Accessibility\n\n- ARIA attributes (`aria-expanded`) dynamically updated on all `\u003csummary\u003e` elements.\n- Escape key support: closes the open `\u003cdetails\u003e` and returns focus to its `\u003csummary\u003e`.\n- Quotations rendered as `\u003cblockquote\u003e` elements for longer text benefit assistive technologies by clearly indicating quoted or cited material. Screen readers announce them as quotations, improving semantic context.\n- The inline `\u003cq\u003e` element is retained for shorter quotations to ensure natural, readable inline flow while remaining accessible.\n\n### No JS\n\nIf JavaScript is disabled, a warning message is displayed and the loader animation, theme-toggler, quotations display and filtering cease to function.\n\n[Back to menu](#menu)\n\n---\n\n## Theme Toggling\n\nThe application includes a dark mode and light mode toggle:\n\n- The current theme state is stored in **local storage** and applied automatically on page reload.\n- Accessible buttons with appropriate ARIA attributes are used to improve usability.\n\n\u003e [!IMPORTANT]\n\u003e Remember to change `const LOCAL_STORAGE_PREFIX` in `js-modules/theme.js` to a unique identifier.\n\n[Back to menu](#menu)\n\n---\n\n## Testing and Compatibility\n\nThe application has been tested on the following platforms and browsers:\n\n- **Operating System**: Windows 11\n- **Browsers**:\n  - Google Chrome\n  - Mozilla Firefox\n  - Microsoft Edge\n\n### Device View Testing\n\nThe layout and functionality have been verified in both browser and device simulation views to ensure responsiveness and usability.\n\n[Back to menu](#menu)\n\n---\n\n## How to Run\n\n1. Clone or download the repository to your local machine.\n2. Open the project folder and start a simple HTTP server (e.g., using `Live Server` in VS Code or Python's `http.server` module).\n3. Open the project in a modern browser (e.g., Chrome, Firefox, or Edge).\n\n[Back to menu](#menu)\n\n---\n\n## Build \u0026 Deployment Setup for `/docs` Folder\n\nIf you want to deploy a minified version of this project to **GitHub Pages**, read on.\n\n### 1. Install Required Packages\n\nRun this once in your project root to install dev dependencies:\n\n```bash\nnpm install\n```\n\n### 2. Run the full build process\n\n\u003e [!IMPORTANT]\n\u003e Any assets not described in `package.json` must be added. In the current project we don't have an `img` folder. If you create one and add images to it, you have to add this to `copy:assets`, e.g.\n\n#### Current `package.json`\n\n```\n\"copy:assets\": \"shx cp -r  site.webmanifest favicon.ico favicon-16x16.png favicon-32x32.png apple-touch-icon.png android-chrome-192x192.png android-chrome-512x512.png docs/\",\n```\n\n#### Updated `package.json` with \"img\"\n\n```\n\"copy:assets\": \"shx cp -r  img site.webmanifest favicon.ico favicon-16x16.png favicon-32x32.png apple-touch-icon.png android-chrome-192x192.png android-chrome-512x512.png docs/\",\n```\n\netc, etc.\n\nThen in the terminal, run:\n\n```bash\nnpm run build\n```\n\n### 3. Deploy to GitHub Pages\n\nOnce you've created a repository and pushed the files,\n\n- go to `https://github.com/[your-name]/[your-project-name]/settings/pages`.\n- Under \"Build and deployment \u003e Branch\" make sure you set the branch to `main` and folder to `/docs`.\n- Click \"Save\".\n\n\u003e [!NOTE]\n\u003e For a detailed description of the build process, configuration files and npm packages see my [GitHub Pages Optimised Build](https://github.com/chrisnajman/github-pages-optimised-build).\n\n[Back to menu](#menu)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisnajman%2Ffilter-quotes-v2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisnajman%2Ffilter-quotes-v2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisnajman%2Ffilter-quotes-v2/lists"}