{"id":15832186,"url":"https://github.com/milliorn/grocery-list","last_synced_at":"2026-05-05T19:33:33.950Z","repository":{"id":41360540,"uuid":"509256648","full_name":"milliorn/grocery-list","owner":"milliorn","description":"Simple, intuitive interface for creating, editing, and deleting grocery items. Users can add items to their shopping list and remove them when they are no longer needed.","archived":false,"fork":false,"pushed_at":"2026-05-01T07:02:36.000Z","size":7285,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-01T09:08:27.503Z","etag":null,"topics":["beginner-friendly","components-react","frontend","grocery-list","list-manager","list-view","localstorage","meal-planner","minimal-ui","open-source","react-hooks","reactjs","responsive","shopping","shopping-list","tailwindcss","todolist","user-interface","vite","webapp"],"latest_commit_sha":null,"homepage":"https://milliorn.github.io/grocery-list/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/milliorn.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"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":"2022-06-30T23:00:35.000Z","updated_at":"2026-05-01T07:02:39.000Z","dependencies_parsed_at":"2026-04-18T04:01:37.196Z","dependency_job_id":null,"html_url":"https://github.com/milliorn/grocery-list","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/milliorn/grocery-list","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milliorn%2Fgrocery-list","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milliorn%2Fgrocery-list/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milliorn%2Fgrocery-list/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milliorn%2Fgrocery-list/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/milliorn","download_url":"https://codeload.github.com/milliorn/grocery-list/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milliorn%2Fgrocery-list/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32665042,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-05T11:29:49.557Z","status":"ssl_error","status_checked_at":"2026-05-05T11:29:48.587Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["beginner-friendly","components-react","frontend","grocery-list","list-manager","list-view","localstorage","meal-planner","minimal-ui","open-source","react-hooks","reactjs","responsive","shopping","shopping-list","tailwindcss","todolist","user-interface","vite","webapp"],"created_at":"2024-10-05T12:41:15.955Z","updated_at":"2026-05-05T19:33:33.939Z","avatar_url":"https://github.com/milliorn.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Grocery List CRUD App\n\n[![pages-build-deployment](https://github.com/milliorn/Grocery-List/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/milliorn/Grocery-List/actions/workflows/pages/pages-build-deployment)\n[![CodeQL](https://github.com/milliorn/Grocery-List/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/milliorn/Grocery-List/actions/workflows/github-code-scanning/codeql)\n\nA fast, fully client-side grocery list manager built with **React 19**, **TypeScript**, **Vite**, and **Tailwind CSS v4**. Add items with quantities, edit them in place, and delete when done. Everything is saved to the browser's `localStorage` with no backend, no account, and only minimal on-demand network activity for lazily loaded features after the initial page load.\n\n**Live demo:** [https://milliorn.github.io/grocery-list/](https://milliorn.github.io/grocery-list/)\n\n---\n\n## Table of Contents\n\n- [Lighthouse Score](#lighthouse-score)\n- [Features](#features)\n- [Tech Stack](#tech-stack)\n- [Project Structure](#project-structure)\n- [Prerequisites](#prerequisites)\n- [Installation](#installation)\n- [Running the App](#running-the-app)\n- [Available Scripts](#available-scripts)\n- [Architecture Overview](#architecture-overview)\n  - [Component Tree](#component-tree)\n  - [Component Reference](#component-reference)\n  - [Data Model](#data-model)\n  - [State Management](#state-management)\n  - [Data Persistence](#data-persistence)\n- [Configuration](#configuration)\n  - [Vite](#vite)\n  - [TypeScript](#typescript)\n  - [Tailwind CSS v4](#tailwind-css-v4)\n  - [PostCSS](#postcss)\n  - [ESLint](#eslint)\n  - [Prettier](#prettier)\n- [Deployment](#deployment)\n- [PWA Support](#pwa-support)\n- [CI/CD and Automation](#cicd-and-automation)\n- [Browser Support](#browser-support)\n- [License](#license)\n- [Acknowledgements](#acknowledgements)\n- [Links](#links)\n\n---\n\n## Lighthouse Score\n\nThe application is optimized for performance, accessibility, best practices, and SEO.\n\n![Lighthouse Score](public/lighthouse.png)\n\n---\n\n## Features\n\n- **Add items:** A togglable form lets you enter an item name and a quantity before saving.\n- **Quantity tracking:** Every item stores a free-text quantity field (e.g. \"2 loaves\", \"500 g\", \"1 dozen\").\n- **Edit items:** A SweetAlert2 dialog pre-fills the current values so you can update the name or quantity without deleting and re-adding.\n- **Delete items:** Remove any item from the list with a single click.\n- **Live item count:** The heading always shows the current number of items remaining in the list.\n- **Form validation:** Client-side checks prevent saving incomplete entries. SweetAlert2 alerts tell you exactly what is missing.\n- **Data persistence:** The list survives page refreshes because it is serialised to `localStorage`. If the storage is corrupt, it is removed gracefully on startup.\n- **Storage error recovery:** If a write to `localStorage` fails (e.g. quota exceeded) the previous list is restored in memory and an error alert is shown.\n- **Responsive layout:** Works on phones, tablets, and desktops. The content area has a maximum width of 672 px and scales text with Tailwind's responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`).\n- **Progressive Web App (PWA):** Includes a `manifest.json` with multiple icon sizes, a theme colour, and `display: standalone` so the app can be added to a device home screen.\n- **Accessibility:** Icon buttons carry `aria-label` attributes so screen-reader users know their purpose.\n- **Dark theme:** The UI uses a zinc-900 dark background with zinc-50 light text.\n- **Random background photo:** A full-screen decorative image is served from [Picsum Photos](https://picsum.photos/) each render.\n\n---\n\n## Tech Stack\n\n| Category             | Technology                  |\n| -------------------- | --------------------------- |\n| UI library           | React                       |\n| Language             | TypeScript                  |\n| Build tool           | Vite                        |\n| React plugin         | @vitejs/plugin-react-swc    |\n| CSS framework        | Tailwind CSS                |\n| Tailwind Vite plugin | @tailwindcss/vite           |\n| Tailwind PostCSS     | @tailwindcss/postcss        |\n| PostCSS              | postcss                     |\n| Autoprefixer         | autoprefixer                |\n| Alert dialogs        | SweetAlert2 (lazy-loaded)   |\n| Linter               | ESLint                      |\n| TypeScript ESLint    | typescript-eslint           |\n| React Hooks lint     | eslint-plugin-react-hooks   |\n| React Refresh lint   | eslint-plugin-react-refresh |\n| Formatter            | Prettier                    |\n| Deployment           | gh-pages                    |\n\nNo backend, no database, no environment variables are required.\n\n---\n\n## Project Structure\n\n```text\ngrocery-list/\n├── .github/\n│   ├── dependabot.yml          # Monthly dependency updates (npm + Actions)\n│   └── workflows/\n│       └── automerge.yml       # Auto-approve/merge Dependabot patch \u0026 minor PRs\n├── public/\n│   ├── android-chrome-192x192.png\n│   ├── android-chrome-512x512.png\n│   ├── apple-touch-icon.png\n│   ├── favicon.ico\n│   ├── lighthouse.png          # Lighthouse score screenshot shown in README\n│   ├── manifest.json           # PWA manifest\n│   └── robots.txt\n├── src/\n│   ├── components/\n│   │   ├── AddGroceryItem.tsx  # Form for adding new items\n│   │   ├── Button.tsx          # Reusable styled button (red / green)\n│   │   ├── DeleteItem.tsx      # Red X icon button\n│   │   ├── EditItem.tsx        # Blue pencil icon button\n│   │   ├── GroceryItem.tsx     # Single item card (quantity + controls)\n│   │   ├── Header.tsx          # App title + Add/Close toggle button\n│   │   ├── ItemEditDelete.tsx  # Groups EditItem and DeleteItem together\n│   │   ├── Items.tsx           # Renders the full list of GroceryItem cards\n│   │   └── ItemsQuantity.tsx   # Displays item name and quantity text\n│   ├── props/\n│   │   ├── AddGroceryItemProps.ts\n│   │   ├── ButtonProps.ts\n│   │   ├── DeleteItemProps.ts\n│   │   ├── EditItemProps.ts\n│   │   ├── GroceryItem.ts      # Core GroceryItem type definition\n│   │   ├── GroceryItemProps.ts\n│   │   ├── HeaderProps.ts\n│   │   ├── ItemEditDeleteProps.ts\n│   │   ├── ItemsProps.ts\n│   │   └── ItemsQuantityProps.ts\n│   ├── styles/\n│   │   └── index.css           # Tailwind import + body background styles\n│   ├── utils/\n│   │   └── getSwal.ts          # Lazy-loads and returns the SweetAlert2 default export\n│   ├── App.tsx                 # Root component; all state and CRUD logic lives here\n│   ├── constants.ts            # STORAGE_KEY constant for localStorage\n│   ├── main.tsx                # React DOM root; mounts App in StrictMode\n│   └── vite-env.d.ts           # Vite client type declarations\n├── .gitignore\n├── .prettierignore\n├── .prettierrc                 # Prettier config: no semicolons, no trailing commas\n├── eslint.config.js            # ESLint flat config (type-aware)\n├── index.html                  # HTML entry point; mounts React via div#root\n├── package.json\n├── package-lock.json\n├── postcss.config.cjs          # PostCSS config: Tailwind + autoprefixer\n├── tsconfig.app.json           # TypeScript config for src/ (strict + extra checks)\n├── tsconfig.json               # Composite root; references app + node configs\n├── tsconfig.node.json          # TypeScript config for Vite config file (Node env)\n└── vite.config.ts              # Vite config: React-SWC plugin, base path, PostCSS\n```\n\n---\n\n## Prerequisites\n\n| Requirement | Minimum version | Notes                                                                  |\n| ----------- | --------------- | ---------------------------------------------------------------------- |\n| Node.js     | 18.x            | React 19 and Vite 7 both require Node 18+. Node 20 LTS is recommended. |\n| npm         | 9.x             | Bundled with Node 18+. Yarn or pnpm also work.                         |\n| Git         | Any recent      | Only needed to clone the repository.                                   |\n\nYou do **not** need any API keys, environment variables, or accounts to run this project.\n\n---\n\n## Installation\n\n### 1. Clone the repository\n\n```bash\ngit clone https://github.com/milliorn/Grocery-List.git\ncd Grocery-List\n```\n\n### 2. Install dependencies\n\n```bash\nnpm install\n```\n\nThis installs both the runtime dependencies (React, SweetAlert2, etc.) and all development tools (Vite, TypeScript, ESLint, Prettier, Tailwind CSS, etc.).\n\n---\n\n## Running the App\n\n**Development server** (hot-reload via Vite + SWC):\n\n```bash\nnpm run dev\n```\n\nOpen [http://localhost:5173/grocery-list/](http://localhost:5173/grocery-list/) in your browser. The page updates instantly as you edit source files, with no manual reload needed.\n\n**Production preview** (test the built output locally):\n\n```bash\nnpm run build\nnpm run preview\n```\n\n---\n\n## Available Scripts\n\nAll scripts are defined in `package.json` and run with `npm run \u003cscript\u003e`.\n\n| Script           | Command                  | Description                                                                                     |\n| ---------------- | ------------------------ | ----------------------------------------------------------------------------------------------- |\n| `dev`            | `vite`                   | Starts the Vite development server with hot module replacement.                                 |\n| `build`          | `tsc -b \u0026\u0026 vite build`   | Type-checks with TypeScript, then produces an optimised production bundle in `dist/`.           |\n| `preview`        | `vite preview`           | Serves the contents of `dist/` locally so you can verify the production build before deploying. |\n| `lint`           | `eslint .`               | Runs ESLint across all TypeScript/TSX files using the flat config in `eslint.config.js`.        |\n| `prettier-check` | `npx prettier . --check` | Checks that all files match the Prettier formatting rules without changing them. Useful in CI.  |\n| `prettier-write` | `npx prettier . --write` | Formats all files in place according to `.prettierrc`. Run this before committing.              |\n| `tsc-watch`      | `tsc -w`                 | Watches TypeScript files and reports type errors on every save without building.                |\n| `deploy-before`  | `npm run build`          | Alias for `build`. Called automatically by the deploy script to ensure the bundle is fresh.     |\n| `deploy`         | `gh-pages -d dist`       | Pushes the `dist/` directory to the `gh-pages` branch, publishing the site to GitHub Pages.     |\n\n---\n\n## Architecture Overview\n\n### Component Tree\n\n```text\nApp                            \u003c- State, CRUD logic, localStorage I/O\n├── Header                     \u003c- Title + toggle button\n│   └── Button                 \u003c- Reusable green/red button\n├── AddGroceryItem             \u003c- Togglable add form (conditionally rendered)\n└── Items                      \u003c- List container\n    └── GroceryItem (x N)      \u003c- One card per item\n        ├── ItemsQuantity      \u003c- Displays item name and quantity\n        └── ItemEditDelete     \u003c- Groups edit + delete icons\n            ├── EditItem       \u003c- Pencil icon -\u003e opens SweetAlert2 edit dialog\n            └── DeleteItem     \u003c- X icon -\u003e calls onDelete handler\n```\n\n### Component Reference\n\n#### `App` (`src/App.tsx`)\n\nThe root component. It owns the entire application state and exposes CRUD callbacks to its children via props.\n\n- **State**:\n  - `items: GroceryItem[]`: the current grocery list.\n  - `showItem: boolean`: controls whether the add form is visible.\n- **On mount** (`useEffect`): reads `localStorage` with the key `\"itemAdded\"`, parses JSON, and initialises `items`. Invalid/corrupt data is silently removed from storage.\n- **`createItem`**: generates a UUID, appends the new item, writes to `localStorage`, and shows a success alert. On storage failure it rolls back to the previous list.\n- **`deleteItem`**: filters out the item by `id`, writes to `localStorage`, and alerts on success or failure.\n- **`updateItem`** (async): opens a SweetAlert2 dialog pre-filled with the current values. On confirmation it maps over the list, replaces the matching item, and persists to `localStorage`.\n\nSweetAlert2 is **lazy-loaded** via dynamic `import()` in all three handlers so the library (~135 KiB) is excluded from the initial bundle and only fetched the first time an alert or dialog fires.\n\n#### `Header` (`src/components/Header.tsx`)\n\nDisplays the \"Grocery List\" heading and a `Button` that toggles the add form. When the form is open the button reads \"Close\" (red); when closed it reads \"Add\" (green).\n\n#### `AddGroceryItem` (`src/components/AddGroceryItem.tsx`)\n\nA controlled form with two text inputs: **Item** and **Quantity**. On submit it validates both fields:\n\n- Both empty → error alert: \"Add item and quantity or close the form.\"\n- Only quantity filled → error alert: \"Add your item.\"\n- Only item filled → error alert: \"Add your quantity.\"\n- Both filled → calls `onSave` and resets the inputs.\n\n#### `Items` (`src/components/Items.tsx`)\n\nMaps the `items` array to a `GroceryItem` component for each entry, passing along `onDelete` and `onEdit` callbacks. Uses `item.id` as the React list key.\n\n#### `GroceryItem` (`src/components/GroceryItem.tsx`)\n\nA single item card laid out as a horizontal flex row. Renders `ItemsQuantity` on the left and `ItemEditDelete` on the right.\n\n#### `ItemsQuantity` (`src/components/ItemsQuantity.tsx`)\n\nRenders two labelled paragraphs:\n\n```text\nItem:     Bread\nQuantity: 2 loaves\n```\n\nResponsive typography scales from the default size up through `sm:`, `md:`, `lg:`, and `xl:` breakpoints.\n\n#### `ItemEditDelete` (`src/components/ItemEditDelete.tsx`)\n\nA simple wrapper that renders `DeleteItem` above `EditItem`, grouping the two action controls for a single grocery item.\n\n#### `EditItem` (`src/components/EditItem.tsx`)\n\nRenders a blue pencil icon (inline SVG) as a `\u003cbutton\u003e`. Clicking it calls `onEdit(item.id)`, which triggers the async SweetAlert2 dialog in `App`.\n\n#### `DeleteItem` (`src/components/DeleteItem.tsx`)\n\nRenders a red X icon (inline SVG) as a `\u003cbutton\u003e`. Clicking it calls `onDelete(item.id)`.\n\n#### `Button` (`src/components/Button.tsx`)\n\nA reusable button component accepting three props:\n\n| Prop      | Type               | Description                                                             |\n| --------- | ------------------ | ----------------------------------------------------------------------- |\n| `color`   | `\"red\" \\| \"green\"` | Selects the Tailwind background class (`bg-red-600` or `bg-green-800`). |\n| `text`    | `string`           | Label rendered inside the button.                                       |\n| `onClick` | `() =\u003e void`       | Click handler.                                                          |\n\n### Data Model\n\nDefined in `src/props/GroceryItem.ts`:\n\n```typescript\nexport type GroceryItem = {\n  id: string // UUID generated by crypto.randomUUID() (native browser API)\n  text: string // Display name of the grocery item (e.g. \"Bread\")\n  quantity: string // Free-text quantity (e.g. \"2 loaves\", \"500 g\", \"1 dozen\")\n}\n```\n\nAll prop interfaces live as individual files in `src/props/` to keep component files clean and make each interface independently importable.\n\n### State Management\n\nThere is no external state management library. All state lives in `App.tsx` using React's built-in `useState` hook:\n\n```text\nitems       -\u003e GroceryItem[]   (the list)\nshowItem    -\u003e boolean         (form visibility)\n```\n\nCallbacks (`createItem`, `deleteItem`, `updateItem`) are defined in `App` and passed down as props to the components that need them. This is a deliberate choice for a small application, avoiding Redux, Zustand, or Context API overhead.\n\n### Data Persistence\n\nItems are stored in `localStorage` under the key `\"itemAdded\"` (defined in `src/constants.ts`).\n\n**Read flow:**\n\n1. On first render (`useEffect` with empty dependency array), `App` calls `localStorage.getItem(\"itemAdded\")`.\n2. If the value is not `null` and not empty, it is parsed with `JSON.parse` and used to initialise `items`.\n3. If parsing throws (corrupt data), the key is removed and `items` stays as an empty array.\n\n**Write flow (every mutation):**\n\n1. State is updated optimistically.\n2. `localStorage.setItem(\"itemAdded\", JSON.stringify(updatedItems))` is called inside a `try/catch`.\n3. On success, a SweetAlert2 success dialog is shown.\n4. On failure (e.g. quota exceeded), the previous state snapshot is restored and an error dialog is shown.\n\n---\n\n## Configuration\n\n### Vite\n\n**File:** `vite.config.ts`\n\n```typescript\nimport { defineConfig } from \"vite\"\nimport react from \"@vitejs/plugin-react-swc\"\n\nexport default defineConfig({\n  plugins: [react()], // SWC-powered JSX transform + Fast Refresh\n  css: {\n    postcss: \"./postcss.config.cjs\" // Points PostCSS at the CJS config file\n  },\n  base: \"/grocery-list/\" // Required for GitHub Pages sub-path hosting\n})\n```\n\nKey points:\n\n- **SWC** replaces Babel as the JSX/TS transpiler, making builds and HMR significantly faster.\n- The `base` option is set to `/grocery-list/` to match the GitHub Pages URL. If you self-host at a root domain, change this to `\"/\"`.\n\n### TypeScript\n\nTwo TypeScript configs are used (project references pattern):\n\n**`tsconfig.json`** is the composite root. It contains no compiler options of its own and simply references the two child configs:\n\n```json\n{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n```\n\n**`tsconfig.app.json`** governs all files in `src/`. Key settings:\n\n| Option                       | Value       | Effect                                                         |\n| ---------------------------- | ----------- | -------------------------------------------------------------- |\n| `target`                     | `ES2020`    | Output syntax (Vite transpiles further for older browsers)     |\n| `strict`                     | `true`      | Enables the full TypeScript strict suite                       |\n| `noEmit`                     | `true`      | TypeScript only type-checks; Vite handles bundling             |\n| `jsx`                        | `react-jsx` | Uses the modern React JSX transform (no `import React` needed) |\n| `moduleResolution`           | `bundler`   | Optimised for Vite/bundler environments                        |\n| `exactOptionalPropertyTypes` | `true`      | Prevents assigning `undefined` to optional props accidentally  |\n| `noImplicitReturns`          | `true`      | Every code path in a function must return a value              |\n| `noUnusedLocals`             | `true`      | Errors on declared but unused variables                        |\n| `noUnusedParameters`         | `true`      | Errors on declared but unused function parameters              |\n| `useUnknownInCatchVariables` | `true`      | Catch clause variable typed as `unknown`, not `any`            |\n\n**`tsconfig.node.json`** governs `vite.config.ts` only. It targets the Node.js environment and uses `tsconfig.node.json` for project-service type information.\n\n### Tailwind CSS v4\n\nThis project uses **Tailwind CSS v4**, which has a fundamentally different setup from v3:\n\n- There is **no `tailwind.config.js`**. Configuration is handled by the plugins themselves.\n- The CSS entry point (`src/styles/index.css`) uses a single import:\n\n  ```css\n  @import \"tailwindcss\";\n  ```\n\n- The Tailwind PostCSS plugin (`@tailwindcss/postcss`) is registered in `postcss.config.cjs`.\n\nAll Tailwind classes used in components are standard utility classes (no custom theme extensions or plugins).\n\n### PostCSS\n\n**File:** `postcss.config.cjs`\n\nWritten as a CommonJS module (`.cjs`) because it is loaded by Vite's PostCSS integration at build time in a Node context:\n\n```javascript\nmodule.exports = {\n  plugins: [\n    require(\"@tailwindcss/postcss\"), // Processes Tailwind utilities\n    require(\"autoprefixer\") // Adds vendor prefixes for cross-browser CSS\n  ]\n}\n```\n\n### ESLint\n\n**File:** `eslint.config.js` (ESLint v9+ flat config)\n\nThe configuration extends:\n\n- `@eslint/js` recommended rules\n- `typescript-eslint` recommended rules (type-aware)\n- `eslint-plugin-react-hooks` recommended rules\n- `eslint-plugin-react-refresh` (warns on non-component exports)\n\nAdditional strictness rules enabled:\n\n| Rule                                               | Level | Effect                                        |\n| -------------------------------------------------- | ----- | --------------------------------------------- |\n| `eqeqeq`                                           | error | Require `===` / `!==`                         |\n| `no-var`                                           | error | No `var`; use `let` or `const`                |\n| `prefer-const`                                     | error | Use `const` when variable is never reassigned |\n| `curly`                                            | error | Require braces for all control structures     |\n| `@typescript-eslint/explicit-function-return-type` | warn  | Explicit return types on functions            |\n| `@typescript-eslint/no-explicit-any`               | error | No `any` type                                 |\n| `@typescript-eslint/strict-boolean-expressions`    | error | No unintended type coercions in conditions    |\n| `@typescript-eslint/no-unsafe-assignment`          | error | No unsafe assignments                         |\n| `@typescript-eslint/prefer-nullish-coalescing`     | error | Use `??` instead of `\\|\\|` where appropriate  |\n| `@typescript-eslint/prefer-optional-chain`         | error | Use `?.` instead of chained `\u0026\u0026` checks       |\n\n`vite.config.ts` has a separate override block that uses `tsconfig.node.json` and `globals.node` instead of `globals.browser`.\n\n### Prettier\n\n**File:** `.prettierrc`\n\n```json\n{\n  \"trailingComma\": \"none\",\n  \"semi\": false\n}\n```\n\nAll other options use Prettier's defaults: double quotes, 80-character print width, 2-space indentation.\n\n**`.prettierignore`** excludes: `build/`, `coverage/`, `node_modules/`, `package-lock.json`, `public/`.\n\n---\n\n## Deployment\n\nThe application is deployed to **GitHub Pages** at:\n\n```text\nhttps://\u003cyour-github\u003e.github.io/grocery-list/\n```\n\n### How it works\n\n1. Run `npm run build` to produce a production bundle in `dist/`.\n2. Run `npm run deploy` to push `dist/` to the `gh-pages` branch using the `gh-pages` npm package.\n3. GitHub Pages serves the `gh-pages` branch automatically.\n\nThe `base: \"/grocery-list/\"` setting in `vite.config.ts` ensures all asset paths are prefixed correctly for the sub-path.\n\n### One-command deploy\n\n```bash\nnpm run deploy\n```\n\nThis runs `deploy-before` (which runs `build`) first, then pushes to `gh-pages`.\n\n### Self-hosting on a custom domain or root path\n\nIf you deploy to the root of a domain (e.g. `https://mysite.com/`) change `base` in `vite.config.ts` from `\"/grocery-list/\"` to `\"/\"`, rebuild, and deploy your `dist/` folder to your web server.\n\n---\n\n## PWA Support\n\nThe app includes a basic Progressive Web App setup:\n\n| File                                | Purpose                                                |\n| ----------------------------------- | ------------------------------------------------------ |\n| `public/manifest.json`              | App name, icon definitions, theme colour, display mode |\n| `public/favicon.ico`                | Browser tab icon (16x16 up to 64x64)                   |\n| `public/apple-touch-icon.png`       | iOS home screen icon                                   |\n| `public/android-chrome-192x192.png` | Android home screen icon (192 px)                      |\n| `public/android-chrome-512x512.png` | Android splash / large icon (512 px)                   |\n\n**`manifest.json` settings:**\n\n| Property           | Value                            |\n| ------------------ | -------------------------------- |\n| `name`             | \"Grocery List\"                   |\n| `short_name`       | \"Grocery List\"                   |\n| `start_url`        | `/grocery-list/`                 |\n| `display`          | `standalone` (no browser chrome) |\n| `theme_color`      | `#18181b` (dark zinc)            |\n| `background_color` | `#18181b`                        |\n\nTo install the app on a mobile device, open the live URL in Chrome or Safari and use \"Add to Home Screen\". The app will launch in standalone mode (no address bar).\n\n\u003e **Note:** This project does not include a Service Worker, so the PWA does not support offline mode.\n\n---\n\n## CI/CD and Automation\n\n### GitHub Actions\n\n**Pages build and deployment** triggers on every push, building and publishing the site to GitHub Pages. Status is shown by the badge at the top of this README.\n\n**CodeQL** provides static security analysis, scanning for known vulnerability patterns in the JavaScript/TypeScript source. Status is shown by the badge at the top of this README.\n\n**Dependabot auto-merge** (`.github/workflows/automerge.yml`):\n\n| Condition             | Action                          |\n| --------------------- | ------------------------------- |\n| Patch or minor update | Auto-approved and squash-merged |\n| Major update          | PR is auto-closed with a label  |\n| `update-type` is null | PR is auto-closed with a label  |\n\n### Dependabot\n\n**File:** `.github/dependabot.yml`\n\nDependabot runs on a **monthly** schedule for two ecosystems:\n\n- `npm` checks all Node.js dependencies. Major version updates are ignored and require manual review.\n- `github-actions` checks all Actions versions used in workflows.\n\n---\n\n## Browser Support\n\nThe project targets **modern evergreen browsers** (Chrome, Firefox, Edge, Safari). The combination of Vite's ES module output, Autoprefixer, and the `ES2020` TypeScript target means:\n\n- No Internet Explorer support.\n- No polyfills are shipped.\n- `localStorage` is used for persistence and is supported by all modern browsers.\n- `crypto.randomUUID()` is used for ID generation. This sets the effective minimum versions:\n\n| Browser | Minimum version | Release date  |\n| ------- | --------------- | ------------- |\n| Chrome  | 92              | July 2021     |\n| Edge    | 92              | July 2021     |\n| Firefox | 95              | December 2021 |\n| Safari  | 15.4            | March 2022    |\n\n---\n\n## License\n\nThis project is licensed under the **MIT License**. See [LICENSE](LICENSE) for the full text.\n\n---\n\n## Acknowledgements\n\n- Background images are courtesy of [Picsum Photos](https://picsum.photos/), a free open-source random image service.\n- Alert dialogs powered by [SweetAlert2](https://sweetalert2.github.io/).\n- Edit and delete icons are inline SVGs sourced from the Font Awesome 5 (`fa`) set via [React Icons](https://react-icons.github.io/react-icons/).\n- Scaffolded from the official Vite React-TS template.\n- Thanks to all contributors and Dependabot for keeping dependencies current.\n\n---\n\n## Links\n\n| Resource      | URL                                                                                  |\n| ------------- | ------------------------------------------------------------------------------------ |\n| Live app      | [https://milliorn.github.io/grocery-list/](https://milliorn.github.io/grocery-list/) |\n| React         | [https://react.dev/](https://react.dev/)                                             |\n| Vite          | [https://vite.dev/](https://vite.dev/)                                               |\n| TypeScript    | [https://www.typescriptlang.org/](https://www.typescriptlang.org/)                   |\n| Tailwind CSS  | [https://tailwindcss.com/](https://tailwindcss.com/)                                 |\n| SweetAlert2   | [https://sweetalert2.github.io/](https://sweetalert2.github.io/)                     |\n| Picsum Photos | [https://picsum.photos/](https://picsum.photos/)                                     |\n| gh-pages      | [https://github.com/tschaub/gh-pages](https://github.com/tschaub/gh-pages)           |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilliorn%2Fgrocery-list","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmilliorn%2Fgrocery-list","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilliorn%2Fgrocery-list/lists"}