Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
760d54c
feat(a11y): Add skeleton kit and async section primitives
OffCrazyFreak Jul 30, 2026
0f8845e
feat(a11y): Add colocated skeletons for products, lists and watchlist
OffCrazyFreak Jul 30, 2026
932344f
feat(a11y): Add per-route loading UI
OffCrazyFreak Jul 30, 2026
eb05594
refactor(a11y): Replace page-body spinners with skeletons
OffCrazyFreak Jul 30, 2026
c095806
refactor(api): Unify the data layer and derive loading from isPending
OffCrazyFreak Jul 30, 2026
b4de002
docs: Document the data fetching and loading UI layer
OffCrazyFreak Jul 30, 2026
a75b3e7
perf(api): Start authed queries on the session, not the profile
OffCrazyFreak Jul 30, 2026
831f01f
docs: Document session gating and the hydration TODO
OffCrazyFreak Jul 30, 2026
853249d
Merge remote-tracking branch 'origin/dev' into feat/loading-skeletons
OffCrazyFreak Jul 30, 2026
8a78654
feat(ui): Add Ko-fi donation entry points
OffCrazyFreak Jul 30, 2026
e544ec6
chore(agents): Expose project skills to Codex
OffCrazyFreak Jul 30, 2026
6a4f403
docs(ui): Document the support flow
OffCrazyFreak Jul 30, 2026
5570a1d
Merge pull request #145 from OffCrazyFreak/feat/support-donations
OffCrazyFreak Jul 30, 2026
0ff4fa1
fix(ui): Refine donation and footer presentation
OffCrazyFreak Jul 30, 2026
be0067e
style(fab): Increase floating action icon size
OffCrazyFreak Jul 30, 2026
809441b
fix(landing): Standardize section spacing
OffCrazyFreak Jul 30, 2026
86a91dc
fix(header): Keep navigation left aligned
OffCrazyFreak Jul 30, 2026
f49d4c7
fix(ui): Keep multi-select search focused after picking an item
OffCrazyFreak Jul 30, 2026
fc3aef2
Merge remote-tracking branch 'origin/dev' into feat/loading-skeletons
OffCrazyFreak Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/better-auth-best-practices
1 change: 1 addition & 0 deletions .agents/skills/create-auth-skill
1 change: 1 addition & 0 deletions .agents/skills/document-subsystem
1 change: 1 addition & 0 deletions .agents/skills/email-and-password-best-practices
1 change: 1 addition & 0 deletions .agents/skills/email-best-practices
1 change: 1 addition & 0 deletions .agents/skills/frontend-design
1 change: 1 addition & 0 deletions .agents/skills/multi-tool-code-review
1 change: 1 addition & 0 deletions .agents/skills/react-email
1 change: 1 addition & 0 deletions .agents/skills/resend
1 change: 1 addition & 0 deletions .agents/skills/sentry-nextjs-sdk
7 changes: 4 additions & 3 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Adds a "Sponsor" button to the repository.

# Ko-fi (0% on one-time tips, best for a general-audience donate button).
# Ko-fi can charge 0% on one-time tips when Contributor mode is disabled.
# Payment processor fees still apply.
ko_fi: disscount

# GitHub Sponsors (0% fees, reaches developers). Needs bank/Stripe + approval.
# Uncomment once the account is set up.
# Uncomment after the GitHub Sponsors profile is approved and publicly available.
# Personal-account sponsors have no GitHub fee. Organization sponsors can incur fees.
# github: OffCrazyFreak
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ Conventions:
- React Query hooks live next to their service in `lib/api/<domain>/`. Feature composition hooks go in the feature's `hooks/`.
- Before generating or redesigning UI, read `frontend/.github/skills/frontend-design/SKILL.md` and follow it.

Data fetching and loading UI, in full in `docs/DATA-FETCHING.md`:

- Each `lib/api/<domain>/` splits into `keys.ts`, `queries.ts` (fetchers) and `hooks.ts` (`queryOptions()` descriptors plus mutation hooks). Reads are descriptors, not hooks, so the React layer picks `useQuery`, `useAuthedQuery`, `useQueries` or a prefetch.
- Never branch on a query's `isLoading`. Under `PersistQueryClientProvider` it reads false with no data while the IndexedDB cache restores, so guards fall through to the error or empty branch. Use `useAuthedQuery`'s `pending`, or `useDataPending(...)`.
- Auth-gated reads go through `useAuthedQuery`, which folds the session into `enabled` and returns `requiresAuth` for the `LoginRequired` gate.
- `staleTime` comes from `CACHE_TIMES` in `lib/query/cache-times.ts`, never a hand-written number.
- Sections render through `AsyncSection`, which fixes the order as pending, error, empty, data.
- A skeleton is a colocated sibling, `<component-name>-skeleton.tsx`, server-renderable, with no hooks, so `loading.tsx` and the client pending branch can share it. It must not re-type the real component's wrapper classes: import them, or share a shell.
- Bars are `h-[1lh]` inside a wrapper carrying the same font classes as the text they replace. `--spacing` is `0.2rem` here, so `h-4` is 12.8px and matches no text size we use.
- `BlockLoadingSpinner` is for buttons and short inline actions only. Content loading gets a skeleton.

Accessibility is where I have had to go back and fix things most often, so check these before you hand UI work over:

- Every icon-only control has an accessible name, and it does not contradict a visible label sitting next to it.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ Big thanks to _[Cijene API](https://github.com/senko/cijene-api/)_ for providing

## Support

If Disscount saves you money or you would like to support its development, you can buy me a coffee. Every bit helps keep the project going and hosted.
If Disscount saves you money, you can support its hosting and further development on Ko-fi. Disscount stays free whether or not you choose to contribute.

[![Support me on Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/disscount)
[![Podrži Disscount na Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/disscount)

## License [![BUSL 1.1][busl-shield]][busl]

Expand Down
503 changes: 503 additions & 0 deletions docs/DATA-FETCHING.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/PWA.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ flowchart LR
```

- `react-query-provider.tsx` uses `PersistQueryClientProvider`. The `QueryClient` sets a default `gcTime` equal to the persister `maxAge` (7 days), so entries are not garbage-collected out of memory before they can be restored from disk.
- `persister.ts` builds a `createAsyncStoragePersister` backed by `idb-keyval` (IndexedDB, larger and safer than localStorage). `maxAge` 7 days, `buster` `"1"` (bump to invalidate all persisted caches after a breaking data-shape change).
- `persister.ts` builds a `createAsyncStoragePersister` backed by `idb-keyval` (IndexedDB, larger and safer than localStorage). `maxAge` 7 days, `buster` `"2"` (bump to invalidate all persisted caches after a breaking data-shape change; it went to `"2"` when query keys moved from stringified params to explicit tuples).
- `cached-query-keys.ts` is the **whitelist**: only queries whose top-level key is in `cijene`, `shoppingLists`, `shoppingListItems`, `watchlist`, `digitalCards`, `pinnedStores`, `pinnedPlaces`, or `users` are persisted, and only when successful. Everything else (for example admin data) is never written to disk. Coming-soon features carry `TODO(offline)` markers here to be added when they ship.
- `purge.ts` (`purgeOfflineCache`) removes the user-specific queries from both the in-memory and IndexedDB caches, guarded so a failed IndexedDB clear never blocks logout. The public `cijene` cache is deliberately kept so public pages stay fast across a logout, and the in-flight cancellation is scoped by the same predicate, so logging out mid-request cannot abort a public price fetch. `user-context.tsx` calls it on **any transition to unauthenticated** (explicit logout, session expiry, revoked cookie, or sign-out in another tab), not just explicit logout, so a previous user's data and queued writes never linger on a shared device.

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ one part of the system.
- [PWA.md](PWA.md) - PWA setup, service worker, offline behaviour, install flow.
- [MOBILE-NAV.md](MOBILE-NAV.md) - mobile bottom nav bar, long-press gestures, tab scrubbing, the shared sheet shell.
- [STATE-PERSISTENCE.md](STATE-PERSISTENCE.md) - how inputs and forms remember state (URL, localStorage drafts, IndexedDB).
- [DATA-FETCHING.md](DATA-FETCHING.md) - data layer, query keys, cache times, loading states and the skeleton system.
- [LANDING.md](LANDING.md) - landing page composition, server-vs-client rendering, SEO, fonts.
- [BRAND.md](BRAND.md) - brand image system (logo, favicon, PWA icons, splash screens, social kit).
- [SUPPORT.md](SUPPORT.md) - Ko-fi support flow, GitHub funding links, and future recognition rules.
148 changes: 148 additions & 0 deletions docs/SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Support and recognition

Disscount is free to use. The support flow gives people a voluntary way to help cover hosting and continued development without creating an account, collecting payment data, or making any feature conditional on payment.

Ko-fi is the only live payment destination today. GitHub Sponsors and public recognition are intentionally future work, because there is not yet an approved Sponsors profile or anyone to list.

## Table of contents

1. [Quick reference](#1-quick-reference)
2. [How the support flow works](#2-how-the-support-flow-works)
3. [Entry points](#3-entry-points)
4. [Automatic and manual work](#4-automatic-and-manual-work)
5. [Key files](#5-key-files)
6. [Payment platform and fees](#6-payment-platform-and-fees)
7. [GitHub repository funding](#7-github-repository-funding)
8. [Accessibility and external-link safety](#8-accessibility-and-external-link-safety)
9. [Verification checklist](#9-verification-checklist)
10. [Gotchas](#10-gotchas)
11. [Future improvements and TODOs](#11-future-improvements-and-todos)

## 1. Quick reference

| Thing | Current value |
| --------------------- | -------------------------------------------------- |
| Live payment platform | [Ko-fi](https://ko-fi.com/disscount) |
| Modal URL | `?modal=donate` |
| Public access | Everyone, including signed-out visitors |
| Sidebar location | `Pomoć i podrška`, after `Kontakt` |
| Footer location | Icon-only support control with an accessible label |
| Data collection | None in Disscount |
| Backend work | None |
| Environment variables | None |

## 2. How the support flow works

The sidebar and footer do not open a local piece of state. They link to `?modal=donate`, matching the rest of Disscount's URL-driven modal system. `ModalRouter` reads the URL, resolves `donate` as a public target, and mounts one `DonationModal` at the root of the app.

The modal explains what a voluntary contribution supports, then opens Ko-fi in a separate tab. Disscount never handles a payment, stores payment information, or calls its backend during this flow.

```mermaid
flowchart LR
Entry[Sidebar or footer control] --> Url[?modal=donate]
Url --> Router[ModalRouter]
Router --> Modal[DonationModal]
Modal --> External[Ko-fi checkout in a new tab]
Modal --> Close[Close, Escape, overlay, Back, or Ne sada]
Close --> Page[Original app page and focus trigger]
```

The modal is public by design. `PUBLIC_MODAL_NAMES` prevents the authentication gate from replacing it with a login prompt for a visitor who is not signed in.

## 3. Entry points

### Sidebar

`supportNavItems` drives the `Pomoć i podrška` group in the app sidebar. The `donate` item comes after `Kontakt`, so it is discoverable without competing with shopping and account navigation. It deliberately has no PWA shortcut metadata because voluntary support is not a core app task.

### Footer

`FooterSupportIcons` maps the same `supportNavItems` data. When it sees a live item, it renders an icon-only button link with the item's label as its accessible name. That makes `Podrži Disscount` compact visually while remaining understandable to screen-reader and keyboard users.

### Deep links

`?modal=donate` can be opened on any route. The existing modal URL helper preserves unrelated query parameters and the hash when it adds or removes the modal parameter. A person can also dismiss the modal with their browser's Back button.

## 4. Automatic and manual work

| Task | Automatic | Manual |
| --------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------- |
| Open the support modal from sidebar or footer | Yes | No |
| Keep the modal public | Yes, through `PUBLIC_MODAL_NAMES` | No |
| Open the payment destination | Yes, in a separate tab | No |
| Receive and process a payment | No | Ko-fi, then its connected Stripe or PayPal account |
| Keep Ko-fi one-time-tip fees at 0% | No | Turn off Ko-fi Contributor mode and accept processor fees |
| Show a GitHub repository Sponsor button | Partly, through `FUNDING.yml` | Confirm the repository setting in GitHub after release |
| Enable GitHub Sponsors | No | Set up and approve the profile before uncommenting its funding entry |
| List supporters or contributors on the landing page | No | Obtain consent and curate the names or logos first |

## 5. Key files

| File | Role |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `frontend/src/constants/donation.ts` | Holds the live Ko-fi URL and the GitHub Sponsors follow-up TODO. |
| `frontend/src/constants/navigation.ts` | Declares the `donate` support-navigation item and the landing recognition TODO. |
| `frontend/src/lib/modal/modal-registry.ts` | Defines, parses, and publicly exposes the `donate` modal target. |
| `frontend/src/components/custom/donation/donation-modal.tsx` | Renders the support copy, Ko-fi link, dismiss action, and focus restoration. |
| `frontend/src/components/custom/modal-router/modal-router.tsx` | Mounts the modal once for the whole app. |
| `frontend/src/components/custom/sidebar/sidebar-support-nav.tsx` | Renders the sidebar support group from shared navigation data. |
| `frontend/src/components/custom/common/footer-support-icons.tsx` | Renders compact footer controls from the same navigation data. |
| `.github/FUNDING.yml` | Configures the repository funding destination shown by GitHub. |
| `README.md` | Gives repository visitors the public Ko-fi support link. |

## 6. Payment platform and fees

Ko-fi is the only linked payment option. The platform can charge 0% service fees on one-time tips when its optional Contributor mode is disabled. Ko-fi starts new creators with Contributor mode enabled, which applies a 5% fee to one-time tips, so check that setting before describing the support flow as zero-fee. Stripe or PayPal processing fees still apply in either mode. [Ko-fi fee details](https://help.ko-fi.com/hc/en-us/articles/360002506494-Does-Ko-fi-take-a-fee)

Buy Me a Coffee is not linked because it charges a 5% platform fee per transaction, in addition to payment processing. Maintaining one live payment choice is also clearer for the people using Disscount. [Buy Me a Coffee fees](https://help.buymeacoffee.com/en/articles/8105744-how-to-calculate-charges-on-your-payment)

## 7. GitHub repository funding

The repository already has `.github/FUNDING.yml` with `ko_fi: disscount`. GitHub reads that file from the default branch to provide a Sponsor button and funding destination on the repository.

The commented `github: OffCrazyFreak` line stays disabled until the GitHub Sponsors profile is approved and public. Before enabling it, confirm that the project meets GitHub's current eligibility requirements, complete the profile and payout setup, then verify the repository setting under Settings, General, Features, Sponsorships. GitHub lists Croatia as a supported payout region. Personal-account sponsorships have no GitHub fee, while organization sponsorships can incur a fee. [GitHub Sponsors overview](https://docs.github.com/en/sponsors/getting-started-with-github-sponsors/about-github-sponsors), [Sponsor button setup](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository)

## 8. Accessibility and external-link safety

The dialog uses the shared Radix-based `ModalShell`, which traps focus while it is open and offers an Escape key and close control. `DonationModal` captures the focused sidebar or footer trigger before opening and restores it after normal dismissal when that trigger is still in the document. A direct deep link has no prior trigger, so it closes without attempting to focus a stale element.

The Ko-fi action is a real anchor with `target="_blank"` and `rel="noopener noreferrer"`. The first opens Ko-fi without replacing the current Disscount page. The second protects the original page from the new tab.

The modal's support icon is decorative and hidden from the accessibility tree. The footer action is visually icon-only but receives the clear accessible label `Podrži Disscount` from the shared navigation data.

## 9. Verification checklist

- [ ] Open `?modal=donate` while signed out and confirm no login prompt appears.
- [ ] Open the sidebar and footer controls and confirm they show the same modal.
- [ ] Close the modal with `Ne sada`, the close control, Escape, the overlay, and browser Back.
- [ ] Confirm closing keeps unrelated query parameters and the URL hash.
- [ ] Use the keyboard to open and close the modal, then confirm focus returns to the original sidebar or footer control.
- [ ] Confirm the Ko-fi control opens `https://ko-fi.com/disscount` in a separate tab.
- [ ] Confirm the footer icon announces itself as `Podrži Disscount`.
- [ ] In GitHub, confirm the repository's Sponsor control leads to Ko-fi after the default branch is updated.

## 10. Gotchas

### The support item must remain public

Do not remove `donate` from `PUBLIC_MODAL_NAMES`. A donation option that first asks a visitor to create an account defeats the purpose of a voluntary, low-friction contribution.

### Do not make the footer control a raw external link

The footer should open the same modal as the sidebar. It gives people a short explanation before sending them to a third-party payment service, while keeping all support copy in one place.

### Do not report a universal 0% fee

Ko-fi's service fee depends on Contributor mode and payment processors charge their own fees. The repository funding-file comments deliberately state these conditions rather than promising a universal 0% rate.

### Do not list people without consent

GitHub sponsorships can be private and Ko-fi supporter data is not a substitute for permission to publish a name or logo. Recognition should use an explicit opt-in and a curated list, never automatic scraping.

## 11. Future improvements and TODOs

- Set up GitHub Sponsors, verify eligibility and payout details, then uncomment the `github: OffCrazyFreak` funding entry and add its live destination to the app.
- Add the `Zajedno gradimo Disscount` landing section only after there are people to recognise.
- Keep that future section in two columns: `Doprinos razvoju` for code contributions and `Podrška projektu` for opted-in financial support.
- Decide on a consent and curation workflow before storing or displaying supporter names, logos, or contribution levels.
- Consider adding voluntary support analytics only after defining a privacy-preserving measurement goal. This first version deliberately sends no tracking event.
3 changes: 2 additions & 1 deletion frontend/src/app/(root)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const metadata: Metadata = {

export default function Home() {
return (
<div className="space-y-14 sm:space-y-20 pb-16">
<div className="space-y-14 pb-10">
<LandingJsonLd />

<HeroSection />
Expand All @@ -30,6 +30,7 @@ export default function Home() {
<StoresSection />
<PwaSection />
<PricingSection />
{/* TODO: Add the "Zajedno gradimo Disscount" contributor and opted-in supporter section here. */}
<FaqSection />
<FinalCtaSection />
</div>
Expand Down
Loading
Loading