diff --git a/.agents/skills/better-auth-best-practices b/.agents/skills/better-auth-best-practices new file mode 120000 index 00000000..b521122b --- /dev/null +++ b/.agents/skills/better-auth-best-practices @@ -0,0 +1 @@ +../../frontend/.agents/skills/better-auth-best-practices \ No newline at end of file diff --git a/.agents/skills/create-auth-skill b/.agents/skills/create-auth-skill new file mode 120000 index 00000000..8f7f6a2a --- /dev/null +++ b/.agents/skills/create-auth-skill @@ -0,0 +1 @@ +../../frontend/.agents/skills/create-auth-skill \ No newline at end of file diff --git a/.agents/skills/document-subsystem b/.agents/skills/document-subsystem new file mode 120000 index 00000000..b11f3e3d --- /dev/null +++ b/.agents/skills/document-subsystem @@ -0,0 +1 @@ +../../.claude/skills/document-subsystem \ No newline at end of file diff --git a/.agents/skills/email-and-password-best-practices b/.agents/skills/email-and-password-best-practices new file mode 120000 index 00000000..9c6c68b1 --- /dev/null +++ b/.agents/skills/email-and-password-best-practices @@ -0,0 +1 @@ +../../frontend/.agents/skills/email-and-password-best-practices \ No newline at end of file diff --git a/.agents/skills/email-best-practices b/.agents/skills/email-best-practices new file mode 120000 index 00000000..0efc93fa --- /dev/null +++ b/.agents/skills/email-best-practices @@ -0,0 +1 @@ +../../frontend/.agents/skills/email-best-practices \ No newline at end of file diff --git a/.agents/skills/frontend-design b/.agents/skills/frontend-design new file mode 120000 index 00000000..191f22ff --- /dev/null +++ b/.agents/skills/frontend-design @@ -0,0 +1 @@ +../../frontend/.github/skills/frontend-design \ No newline at end of file diff --git a/.agents/skills/multi-tool-code-review b/.agents/skills/multi-tool-code-review new file mode 120000 index 00000000..2ce6e9df --- /dev/null +++ b/.agents/skills/multi-tool-code-review @@ -0,0 +1 @@ +../../.claude/skills/multi-tool-code-review \ No newline at end of file diff --git a/.agents/skills/react-email b/.agents/skills/react-email new file mode 120000 index 00000000..bcd0cee2 --- /dev/null +++ b/.agents/skills/react-email @@ -0,0 +1 @@ +../../frontend/.agents/skills/react-email \ No newline at end of file diff --git a/.agents/skills/resend b/.agents/skills/resend new file mode 120000 index 00000000..23134afe --- /dev/null +++ b/.agents/skills/resend @@ -0,0 +1 @@ +../../frontend/.agents/skills/resend \ No newline at end of file diff --git a/.agents/skills/sentry-nextjs-sdk b/.agents/skills/sentry-nextjs-sdk new file mode 120000 index 00000000..80daaa6b --- /dev/null +++ b/.agents/skills/sentry-nextjs-sdk @@ -0,0 +1 @@ +../../frontend/.agents/skills/sentry-nextjs-sdk \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d163da4c..4d6f62ef 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -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 diff --git a/README.md b/README.md index b2bcba9f..df66ea03 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/docs/README.md b/docs/README.md index 16ef980a..03eb36bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,3 +10,4 @@ one part of the system. - [STATE-PERSISTENCE.md](STATE-PERSISTENCE.md) - how inputs and forms remember state (URL, localStorage drafts, IndexedDB). - [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. diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md new file mode 100644 index 00000000..3590063f --- /dev/null +++ b/docs/SUPPORT.md @@ -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. diff --git a/frontend/src/app/(root)/data/landing.ts b/frontend/src/app/(root)/data/landing.ts index e3b7d4a2..13bd7966 100644 --- a/frontend/src/app/(root)/data/landing.ts +++ b/frontend/src/app/(root)/data/landing.ts @@ -14,6 +14,10 @@ export const tagLines: string[] = [ "Zaboravi na kataloge i letke!", ]; +// TODO: Add a "Zajedno gradimo Disscount" section once there are contributors +// to show. It should keep code contributions and opted-in financial support in +// separate columns: "Doprinos razvoju" and "Podrška projektu". + export interface IHowItWorksStep { title: string; description: string; diff --git a/frontend/src/components/custom/common/footer-support-icons.tsx b/frontend/src/components/custom/common/footer-support-icons.tsx index 537e57ad..5f332ea3 100644 --- a/frontend/src/components/custom/common/footer-support-icons.tsx +++ b/frontend/src/components/custom/common/footer-support-icons.tsx @@ -7,7 +7,7 @@ const LIVE_CLASS = "text-muted-foreground hover:text-primary transition-all hover:scale-110"; const DISABLED_CLASS = "text-muted-foreground/50"; -/** Feedback entry icons, sharing supportNavItems with the sidebar group. */ +/** Support entry icons, sharing supportNavItems with the sidebar group. */ export default function FooterSupportIcons() { return (
diff --git a/frontend/src/components/custom/donation/donation-modal.tsx b/frontend/src/components/custom/donation/donation-modal.tsx new file mode 100644 index 00000000..1e4f43ac --- /dev/null +++ b/frontend/src/components/custom/donation/donation-modal.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { Coffee, HeartHandshake } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { ModalShell } from "@/components/custom/modal/modal-shell"; +import { KO_FI_URL } from "@/constants/donation"; +import { closeModalUrl } from "@/lib/modal/modal-navigation"; + +interface IDonationModalProps { + open: boolean; +} + +/** Public support prompt, reachable from the sidebar and footer. */ +export default function DonationModal({ open }: IDonationModalProps) { + const triggerRef = useRef(null); + + useEffect(() => { + function rememberTrigger(target: EventTarget | null) { + if (!(target instanceof Element)) return; + + const trigger = target.closest('a[href*="modal=donate"]'); + if (trigger instanceof HTMLElement) triggerRef.current = trigger; + } + + function handlePointerDown(event: PointerEvent) { + rememberTrigger(event.target); + } + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Enter" || event.key === " ") { + rememberTrigger(document.activeElement); + } + } + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + function handleClose() { + closeModalUrl(); + + const trigger = triggerRef.current; + triggerRef.current = null; + + window.requestAnimationFrame(() => { + if (trigger?.isConnected) trigger.focus(); + }); + } + + return ( + !isOpen && handleClose()} + title="Podrži Disscount" + description="Disscount je besplatan i takav ostaje. Ako ti pomaže pri kupnji, dobrovoljna podrška pomaže pokriti hosting i daljnji razvoj." + size="sm" + centered + hero={ +
+ +
+ } + footer={ +
+ + + +
+ } + /> + ); +} diff --git a/frontend/src/components/custom/modal-router/modal-router.tsx b/frontend/src/components/custom/modal-router/modal-router.tsx index 0eba46f1..c0b5be82 100644 --- a/frontend/src/components/custom/modal-router/modal-router.tsx +++ b/frontend/src/components/custom/modal-router/modal-router.tsx @@ -11,6 +11,7 @@ import OnboardingGate from "@/components/custom/settings/onboarding/components/o import ResetPasswordModal from "@/components/custom/auth/reset-password-modal"; import AuthStatusModal from "@/components/custom/auth/auth-status-modal"; import ContactModal from "@/components/custom/contact/contact-modal"; +import DonationModal from "@/components/custom/donation/donation-modal"; import ProductActionsOutlet from "@/components/custom/modal-router/product-actions-outlet"; import { AUTH_MODAL_NAMES, @@ -113,6 +114,7 @@ export default function ModalRouter() { /> + diff --git a/frontend/src/components/custom/sidebar/sidebar-support-nav.tsx b/frontend/src/components/custom/sidebar/sidebar-support-nav.tsx index eef8fc61..e8d0e284 100644 --- a/frontend/src/components/custom/sidebar/sidebar-support-nav.tsx +++ b/frontend/src/components/custom/sidebar/sidebar-support-nav.tsx @@ -11,7 +11,7 @@ import SidebarNavItem from "@/components/custom/sidebar/sidebar-nav-item"; import { supportNavItems, isNavItemLocked } from "@/constants/navigation"; import { useUser } from "@/context/user-context"; -/** Feedback entry points: ideas board, bug reports and contact. */ +/** Support entry points: feedback, contact and voluntary project support. */ export default function SidebarSupportNav() { const { user } = useUser(); diff --git a/frontend/src/constants/donation.ts b/frontend/src/constants/donation.ts new file mode 100644 index 00000000..ee1cecf8 --- /dev/null +++ b/frontend/src/constants/donation.ts @@ -0,0 +1,4 @@ +/** Public support destination, kept separate from contact channels. */ +export const KO_FI_URL = "https://ko-fi.com/disscount"; + +// TODO: Add the final GitHub Sponsors profile URL after the account is approved. diff --git a/frontend/src/constants/navigation.ts b/frontend/src/constants/navigation.ts index ee0bce3f..e3a59955 100644 --- a/frontend/src/constants/navigation.ts +++ b/frontend/src/constants/navigation.ts @@ -12,6 +12,7 @@ import { ChartNoAxesCombined, LayoutDashboard, Bug, + HeartHandshake, Mail, Package, House, @@ -198,7 +199,7 @@ export const productNavItems: INavigationItem[] = [ }, ]; -// Feedback entry points, shared with the footer; coming-soon until their pages/modals ship. +// Support entry points, shared with the footer; coming-soon until their pages/modals ship. export const supportNavItems: INavigationItem[] = [ { id: "suggestions", @@ -224,6 +225,14 @@ export const supportNavItems: INavigationItem[] = [ label: "Kontakt", icon: Mail, + showInHeader: false, + }, + { + id: "donate", + href: "?modal=donate", + label: "Podrži Disscount", + icon: HeartHandshake, + showInHeader: false, }, ]; diff --git a/frontend/src/lib/modal/modal-registry.ts b/frontend/src/lib/modal/modal-registry.ts index d6719f60..9ae54fe4 100644 --- a/frontend/src/lib/modal/modal-registry.ts +++ b/frontend/src/lib/modal/modal-registry.ts @@ -19,6 +19,7 @@ export type ModalTarget = | { name: "email-verified" } | { name: "email-changed" } | { name: "contact" } + | { name: "donate" } | { name: "onboarding"; mode: "required" | "replay" } | { name: "settings"; tab: SettingsTab } | { name: "shopping-list"; action: "new" } @@ -38,6 +39,7 @@ export const PUBLIC_MODAL_NAMES = [ "email-verified", "email-changed", "contact", + "donate", // Two of its four actions need no account, and the gated two gate themselves. "product-actions", ] as const; @@ -64,6 +66,7 @@ export function parseModalParam( case "email-verified": case "email-changed": case "contact": + case "donate": return { name }; case "onboarding": return { name, mode: sub === "replay" ? "replay" : "required" };