feat(web): lien « Buy me a coffee » (nav, réglages, funding)

- constante BUYMEACOFFEE_URL ; item de nav « Offrez-moi un café »
- section support dans les Réglages + i18n EN/FR
- champ `funding` dans le package.json publié ; mentions README EN/FR

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 17:02:30 +02:00
parent c88ae9b2ae
commit 342430c405
9 changed files with 64 additions and 3 deletions

View File

@@ -255,6 +255,12 @@ node packages/server/scripts/acceptance-p4.mjs # Web Push + commande WS `answe
node packages/server/scripts/acceptance-p5.mjs # groupes de travail : CRUD + broadcast WS + CASCADE node packages/server/scripts/acceptance-p5.mjs # groupes de travail : CRUD + broadcast WS + CASCADE
``` ```
## Soutenir le projet
Arboretum est un projet personnel libre et auto-financé. S'il vous fait gagner du temps, vous pouvez soutenir son développement :
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-johanleroy-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/johanleroy)
## Licence ## Licence
MIT — voir [LICENSE](LICENSE). MIT — voir [LICENSE](LICENSE).

View File

@@ -255,6 +255,12 @@ node packages/server/scripts/acceptance-p4.mjs # Web Push + WS `answer` comman
node packages/server/scripts/acceptance-p5.mjs # work groups: CRUD + WS broadcast + CASCADE node packages/server/scripts/acceptance-p5.mjs # work groups: CRUD + WS broadcast + CASCADE
``` ```
## Support
Arboretum is a free, self-funded side project. If it saves you time, you can support its development:
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-johanleroy-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/johanleroy)
## License ## License
MIT — see [LICENSE](LICENSE). MIT — see [LICENSE](LICENSE).

View File

@@ -14,6 +14,10 @@
"bugs": { "bugs": {
"url": "https://git.lidge.fr/johanleroy/arboretum/issues" "url": "https://git.lidge.fr/johanleroy/arboretum/issues"
}, },
"funding": {
"type": "buymeacoffee",
"url": "https://buymeacoffee.com/johanleroy"
},
"keywords": [ "keywords": [
"git", "git",
"worktree", "worktree",

View File

@@ -1,10 +1,10 @@
import { computed, type Component } from 'vue'; import { computed, type Component } from 'vue';
import { useRoute, type RouteLocationRaw } from 'vue-router'; import { useRoute, type RouteLocationRaw } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Boxes, GitBranch, LifeBuoy, Settings, TerminalSquare } from '@lucide/vue'; import { Boxes, Coffee, GitBranch, LifeBuoy, Settings, TerminalSquare } from '@lucide/vue';
import { useSessionsStore } from '../stores/sessions'; import { useSessionsStore } from '../stores/sessions';
import GiteaIcon from '../components/ui/GiteaIcon.vue'; import GiteaIcon from '../components/ui/GiteaIcon.vue';
import { REPO_SOURCE_URL } from '../lib/constants'; import { BUYMEACOFFEE_URL, REPO_SOURCE_URL } from '../lib/constants';
export interface NavEntry { export interface NavEntry {
key: string; key: string;
@@ -41,6 +41,7 @@ export function useNav() {
{ key: 'settings', to: { name: 'settings' }, icon: Settings, label: t('nav.settings'), match: ['settings'], badge: 0 }, { key: 'settings', to: { name: 'settings' }, icon: Settings, label: t('nav.settings'), match: ['settings'], badge: 0 },
{ key: 'help', to: { name: 'help' }, icon: LifeBuoy, label: t('nav.help'), match: ['help'], badge: 0 }, { key: 'help', to: { name: 'help' }, icon: LifeBuoy, label: t('nav.help'), match: ['help'], badge: 0 },
{ key: 'gitea', href: REPO_SOURCE_URL, icon: GiteaIcon, label: t('nav.gitea'), match: [], badge: 0 }, { key: 'gitea', href: REPO_SOURCE_URL, icon: GiteaIcon, label: t('nav.gitea'), match: [], badge: 0 },
{ key: 'buymeacoffee', href: BUYMEACOFFEE_URL, icon: Coffee, label: t('nav.buymeacoffee'), match: [], badge: 0 },
]); ]);
const isActive = (match: string[]): boolean => match.includes(String(route.name)); const isActive = (match: string[]): boolean => match.includes(String(route.name));

View File

@@ -182,6 +182,7 @@ export default {
help: 'Help', help: 'Help',
more: 'More', more: 'More',
gitea: 'Source code', gitea: 'Source code',
buymeacoffee: 'Buy me a coffee',
waiting: 'waiting', waiting: 'waiting',
}, },
controls: { controls: {
@@ -364,6 +365,10 @@ export default {
vapidContact: 'VAPID contact', vapidContact: 'VAPID contact',
none: 'none', none: 'none',
flagHint: 'CLI flag: {flag}', flagHint: 'CLI flag: {flag}',
// Support
support: 'Support',
supportHint: 'Arboretum is a free, self-funded side project. If it saves you time, you can support its development.',
supportCta: 'Buy me a coffee',
}, },
help: { help: {
title: 'Help', title: 'Help',

View File

@@ -185,6 +185,7 @@ const fr: typeof en = {
help: 'Aide', help: 'Aide',
more: 'Plus', more: 'Plus',
gitea: 'Code source', gitea: 'Code source',
buymeacoffee: 'Offrez-moi un café',
waiting: 'en attente', waiting: 'en attente',
}, },
controls: { controls: {
@@ -367,6 +368,10 @@ const fr: typeof en = {
vapidContact: 'Contact VAPID', vapidContact: 'Contact VAPID',
none: 'aucune', none: 'aucune',
flagHint: 'Flag CLI : {flag}', flagHint: 'Flag CLI : {flag}',
// Soutien
support: 'Soutenir le projet',
supportHint: 'Arboretum est un projet personnel libre et auto-financé. Sil vous fait gagner du temps, vous pouvez soutenir son développement.',
supportCta: 'Offrez-moi un café',
}, },
help: { help: {
title: 'Aide', title: 'Aide',

View File

@@ -6,3 +6,9 @@
* tous les utilisateurs de la solution. * tous les utilisateurs de la solution.
*/ */
export const REPO_SOURCE_URL = 'https://git.lidge.fr/johanleroy/arboretum'; export const REPO_SOURCE_URL = 'https://git.lidge.fr/johanleroy/arboretum';
/**
* Lien « Buy me a coffee » affiché dans la navigation et la page Réglages.
* En dur (pas un réglage) : page de don personnelle, identique pour tous.
*/
export const BUYMEACOFFEE_URL = 'https://buymeacoffee.com/johanleroy';

View File

@@ -18,6 +18,15 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@layer base {
/* Tailwind v4 ne met plus cursor:pointer sur les boutons → on le rétablit
(sauf désactivés, qui gardent le curseur par défaut / not-allowed). */
button:not(:disabled),
[role='button']:not([aria-disabled='true']) {
cursor: pointer;
}
}
@layer components { @layer components {
/* Champs : rayon unifié + anneau de focus visible (absent auparavant). */ /* Champs : rayon unifié + anneau de focus visible (absent auparavant). */
.input { .input {

View File

@@ -178,13 +178,31 @@
</dl> </dl>
<SkeletonRow v-else /> <SkeletonRow v-else />
</section> </section>
<!-- Support -->
<section class="card flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
<Coffee :size="16" /> {{ t('settings.support') }}
</h2>
<p class="text-xs text-zinc-500">{{ t('settings.supportHint') }}</p>
<div>
<a
:href="BUYMEACOFFEE_URL"
target="_blank"
rel="noopener noreferrer"
class="inline-flex h-9 items-center justify-center gap-1.5 rounded-lg border border-zinc-700 bg-zinc-800 px-3 text-sm font-medium text-zinc-200 transition-colors hover:bg-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950"
>
<Coffee :size="16" /> {{ t('settings.supportCta') }}
</a>
</div>
</section>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Bell, BellOff, Check, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Trash2, X } from '@lucide/vue'; import { Bell, BellOff, Check, Coffee, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Trash2, X } from '@lucide/vue';
import type { import type {
AuditLogEntry, AuditLogEntry,
AuditLogsResponse, AuditLogsResponse,
@@ -195,6 +213,7 @@ import type {
TokensListResponse, TokensListResponse,
} from '@arboretum/shared'; } from '@arboretum/shared';
import { api, ApiError } from '../lib/api'; import { api, ApiError } from '../lib/api';
import { BUYMEACOFFEE_URL } from '../lib/constants';
import { useSettingsStore } from '../stores/settings'; import { useSettingsStore } from '../stores/settings';
import { usePushStore } from '../stores/push'; import { usePushStore } from '../stores/push';
import { useToastsStore } from '../stores/toasts'; import { useToastsStore } from '../stores/toasts';