rails_tipjar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +246 -0
- data/Rakefile +4 -0
- data/app/assets/javascripts/tipjar_controller.js +154 -0
- data/app/assets/stylesheets/tipjar.css +213 -0
- data/app/helpers/tipjar_helper.rb +93 -0
- data/app/views/tipjar/_button.html.erb +16 -0
- data/app/views/tipjar/_modal.html.erb +74 -0
- data/config/importmap.rb +1 -0
- data/lib/generators/tipjar/install/install_generator.rb +92 -0
- data/lib/generators/tipjar/install/templates/tipjar.rb +53 -0
- data/lib/rails_tipjar/configuration.rb +88 -0
- data/lib/rails_tipjar/engine.rb +26 -0
- data/lib/rails_tipjar/version.rb +5 -0
- data/lib/rails_tipjar.rb +22 -0
- data/sig/rails_tipjar.rbs +4 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a942f0f33f3ae7a78eda233a1dff1ce1a883a144ac1cad2a31e9849f212b708
|
4
|
+
data.tar.gz: f00214075a3e9008ddc7aa9b85d46caca6a6792ebbc185407c5dae07ccc114f8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7a437aedf1dd798d8bb7e76d364236f19c80b1ee3295c72df64477cbaa1ed1f4e51f494a32e5a53da8fa86fd42a7376dc7c53110e916fcd24020f9d6c6739819
|
7
|
+
data.tar.gz: 38d10e40392878cf99f43ed646a41401630f97340b97145b02b49ce9a48d8b49a716d5c4c2cb4bf1d59d873614cf5f404e4740d894cbb32045ac402d05f7de86
|
data/README.md
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
# RailsTipjar
|
2
|
+
|
3
|
+
A reusable, customizable tip jar feature for Rails applications using Stripe Payment Links. Add a beautiful tip jar to any Rails app with just a few lines of code!
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- ๐จ Customizable floating button with multiple icon options
|
8
|
+
- ๐ณ Stripe Payment Links integration (no backend payment processing needed)
|
9
|
+
- ๐ฑ Fully responsive design
|
10
|
+
- ๐ฏ Multiple positioning options
|
11
|
+
- ๐ Light/Dark theme support
|
12
|
+
- ๐ Built-in analytics tracking
|
13
|
+
- โก Stimulus.js powered interactions
|
14
|
+
- ๐ง Zero configuration to get started
|
15
|
+
|
16
|
+
## Why Stripe Payment Links?
|
17
|
+
|
18
|
+
- **No payment code** in your Rails apps
|
19
|
+
- **PCI compliant** automatically
|
20
|
+
- **No monthly fees** - pay only 2.9% + 30ยข per transaction
|
21
|
+
- **International support** with multi-currency
|
22
|
+
- **Professional checkout** experience
|
23
|
+
- **Automatic receipts** and tax handling
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
Add this gem to your Rails application's Gemfile:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# From GitHub (recommended during development)
|
31
|
+
gem 'rails_tipjar', git: 'https://github.com/justinpaulson/rails_tipjar'
|
32
|
+
|
33
|
+
# Or from RubyGems (when published)
|
34
|
+
gem 'rails_tipjar'
|
35
|
+
```
|
36
|
+
|
37
|
+
Then execute:
|
38
|
+
|
39
|
+
```bash
|
40
|
+
bundle install
|
41
|
+
rails generate tipjar:install
|
42
|
+
```
|
43
|
+
|
44
|
+
## Quick Start
|
45
|
+
|
46
|
+
### 1. Create Stripe Payment Links
|
47
|
+
|
48
|
+
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/payment-links)
|
49
|
+
2. Create payment links for different tip amounts
|
50
|
+
3. Set them to "Donation" mode
|
51
|
+
4. Copy the generated URLs
|
52
|
+
|
53
|
+
### 2. Configure the Gem
|
54
|
+
|
55
|
+
Edit `config/initializers/tipjar.rb`:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
RailsTipjar.configure do |config|
|
59
|
+
config.payment_links = {
|
60
|
+
small: "https://buy.stripe.com/your-5-dollar-link",
|
61
|
+
medium: "https://buy.stripe.com/your-10-dollar-link",
|
62
|
+
large: "https://buy.stripe.com/your-25-dollar-link"
|
63
|
+
}
|
64
|
+
|
65
|
+
config.position = :bottom_right
|
66
|
+
config.icon = :coffee
|
67
|
+
config.button_text = "Buy me a coffee"
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
### 3. Add to Your Layout
|
72
|
+
|
73
|
+
In `app/views/layouts/application.html.erb`:
|
74
|
+
|
75
|
+
```erb
|
76
|
+
<!DOCTYPE html>
|
77
|
+
<html>
|
78
|
+
<head>
|
79
|
+
<!-- your head content -->
|
80
|
+
</head>
|
81
|
+
<body>
|
82
|
+
<%= yield %>
|
83
|
+
|
84
|
+
<!-- Add tip jar button -->
|
85
|
+
<%= tipjar_button %>
|
86
|
+
</body>
|
87
|
+
</html>
|
88
|
+
```
|
89
|
+
|
90
|
+
That's it! You now have a working tip jar on your site.
|
91
|
+
|
92
|
+
## Configuration Options
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
RailsTipjar.configure do |config|
|
96
|
+
# Payment Links (required)
|
97
|
+
config.payment_links = {
|
98
|
+
small: "...",
|
99
|
+
medium: "...",
|
100
|
+
large: "...",
|
101
|
+
custom: "..." # Optional: for custom amounts
|
102
|
+
}
|
103
|
+
|
104
|
+
# Button position
|
105
|
+
# Options: :bottom_right, :bottom_left, :top_right, :top_left
|
106
|
+
config.position = :bottom_right
|
107
|
+
|
108
|
+
# Button icon
|
109
|
+
# Options: :coffee, :heart, :star, :dollar, or custom SVG string
|
110
|
+
config.icon = :coffee
|
111
|
+
|
112
|
+
# Theme
|
113
|
+
# Options: :light, :dark, :auto
|
114
|
+
config.theme = :light
|
115
|
+
|
116
|
+
# Button text (shown on hover)
|
117
|
+
config.button_text = "Buy me a coffee"
|
118
|
+
|
119
|
+
# Modal customization
|
120
|
+
config.modal_title = "Support my work"
|
121
|
+
config.modal_description = "Your support helps me continue creating."
|
122
|
+
|
123
|
+
# Tip amounts shown in modal
|
124
|
+
config.custom_amounts = [
|
125
|
+
{ amount: 5, label: "$5", default: false },
|
126
|
+
{ amount: 10, label: "$10", default: true },
|
127
|
+
{ amount: 25, label: "$25", default: false },
|
128
|
+
{ amount: 50, label: "$50", default: false }
|
129
|
+
]
|
130
|
+
|
131
|
+
# Additional styling
|
132
|
+
config.button_class = "custom-class"
|
133
|
+
config.z_index = 1000
|
134
|
+
|
135
|
+
# Analytics
|
136
|
+
config.analytics_enabled = true # Works with GA4 and Plausible
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
## Usage
|
141
|
+
|
142
|
+
### Basic Button
|
143
|
+
|
144
|
+
```erb
|
145
|
+
<!-- Default button with all config options -->
|
146
|
+
<%= tipjar_button %>
|
147
|
+
|
148
|
+
<!-- Override position for specific button -->
|
149
|
+
<%= tipjar_button position: :top_left %>
|
150
|
+
|
151
|
+
<!-- Add custom CSS classes -->
|
152
|
+
<%= tipjar_button class: "my-custom-class" %>
|
153
|
+
```
|
154
|
+
|
155
|
+
### Custom Links
|
156
|
+
|
157
|
+
```erb
|
158
|
+
<!-- Create a custom tip link -->
|
159
|
+
<%= tipjar_link "Support this project", amount: :large %>
|
160
|
+
|
161
|
+
<!-- With custom styling -->
|
162
|
+
<%= tipjar_link "Tip $5", amount: :small, class: "btn btn-primary" %>
|
163
|
+
```
|
164
|
+
|
165
|
+
### Modal Only
|
166
|
+
|
167
|
+
```erb
|
168
|
+
<!-- Render just the modal (for custom triggers) -->
|
169
|
+
<%= tipjar_modal %>
|
170
|
+
```
|
171
|
+
|
172
|
+
## Styling
|
173
|
+
|
174
|
+
The gem includes default styles that work with most Rails applications. You can override styles by targeting these CSS classes:
|
175
|
+
|
176
|
+
```css
|
177
|
+
/* Button */
|
178
|
+
.tipjar-button { }
|
179
|
+
.tipjar-button-icon { }
|
180
|
+
.tipjar-button-text { }
|
181
|
+
|
182
|
+
/* Modal */
|
183
|
+
.tipjar-modal { }
|
184
|
+
.tipjar-modal-content { }
|
185
|
+
.tipjar-modal-header { }
|
186
|
+
.tipjar-amount-button { }
|
187
|
+
.tipjar-amount-active { }
|
188
|
+
.tipjar-submit-button { }
|
189
|
+
```
|
190
|
+
|
191
|
+
## Analytics
|
192
|
+
|
193
|
+
When `analytics_enabled` is true, the following events are tracked:
|
194
|
+
|
195
|
+
- `tipjar_loaded` - Button loaded on page
|
196
|
+
- `tipjar_modal_opened` - User opened the modal
|
197
|
+
- `tipjar_amount_selected` - User selected an amount
|
198
|
+
- `tipjar_submitted` - User clicked to proceed to payment
|
199
|
+
|
200
|
+
Works automatically with:
|
201
|
+
- Google Analytics 4 (gtag)
|
202
|
+
- Plausible Analytics
|
203
|
+
- Custom events via `document.addEventListener('tipjar:event', ...)`
|
204
|
+
|
205
|
+
## Development
|
206
|
+
|
207
|
+
After checking out the repo:
|
208
|
+
|
209
|
+
```bash
|
210
|
+
cd rails_tipjar
|
211
|
+
bundle install
|
212
|
+
```
|
213
|
+
|
214
|
+
To test in a Rails app, add to your Gemfile:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
gem 'rails_tipjar', path: '../path/to/rails_tipjar'
|
218
|
+
```
|
219
|
+
|
220
|
+
## Troubleshooting
|
221
|
+
|
222
|
+
### Button not appearing
|
223
|
+
|
224
|
+
1. Check that `<%= tipjar_button %>` is in your layout
|
225
|
+
2. Verify Stimulus is working in your app
|
226
|
+
3. Check browser console for JavaScript errors
|
227
|
+
|
228
|
+
### Modal not opening
|
229
|
+
|
230
|
+
1. Ensure Stimulus controller is registered
|
231
|
+
2. Check that tipjar.css is being loaded
|
232
|
+
3. Verify no z-index conflicts with your existing styles
|
233
|
+
|
234
|
+
### Payment links not working
|
235
|
+
|
236
|
+
1. Ensure your Stripe Payment Links are active
|
237
|
+
2. Check that URLs are correctly configured in initializer
|
238
|
+
3. Test links directly in browser
|
239
|
+
|
240
|
+
## Contributing
|
241
|
+
|
242
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/justinpaulson/rails_tipjar.
|
243
|
+
|
244
|
+
## License
|
245
|
+
|
246
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["button", "modal", "backdrop", "amountButton"]
|
5
|
+
static values = {
|
6
|
+
paymentLinks: Object,
|
7
|
+
theme: String,
|
8
|
+
modalTitle: String,
|
9
|
+
modalDescription: String,
|
10
|
+
customAmounts: Array,
|
11
|
+
analyticsEnabled: Boolean
|
12
|
+
}
|
13
|
+
|
14
|
+
connect() {
|
15
|
+
this.selectedAmount = null
|
16
|
+
this.setupEventListeners()
|
17
|
+
|
18
|
+
if (this.analyticsEnabledValue) {
|
19
|
+
this.trackEvent("tipjar_loaded")
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
setupEventListeners() {
|
24
|
+
// Close modal on escape key
|
25
|
+
document.addEventListener("keydown", this.handleEscapeKey.bind(this))
|
26
|
+
|
27
|
+
// Close modal on backdrop click
|
28
|
+
if (this.hasBackdropTarget) {
|
29
|
+
this.backdropTarget.addEventListener("click", this.closeModal.bind(this))
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
disconnect() {
|
34
|
+
document.removeEventListener("keydown", this.handleEscapeKey.bind(this))
|
35
|
+
}
|
36
|
+
|
37
|
+
handleEscapeKey(event) {
|
38
|
+
if (event.key === "Escape" && this.hasModalTarget) {
|
39
|
+
this.closeModal()
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
openModal(event) {
|
44
|
+
event.preventDefault()
|
45
|
+
|
46
|
+
if (this.hasModalTarget) {
|
47
|
+
this.modalTarget.classList.remove("hidden")
|
48
|
+
this.modalTarget.classList.add("flex")
|
49
|
+
document.body.style.overflow = "hidden"
|
50
|
+
|
51
|
+
if (this.analyticsEnabledValue) {
|
52
|
+
this.trackEvent("tipjar_modal_opened")
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
closeModal() {
|
58
|
+
if (this.hasModalTarget) {
|
59
|
+
this.modalTarget.classList.add("hidden")
|
60
|
+
this.modalTarget.classList.remove("flex")
|
61
|
+
document.body.style.overflow = ""
|
62
|
+
|
63
|
+
if (this.analyticsEnabledValue) {
|
64
|
+
this.trackEvent("tipjar_modal_closed")
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
selectAmount(event) {
|
70
|
+
const button = event.currentTarget
|
71
|
+
const amount = button.dataset.amount
|
72
|
+
|
73
|
+
// Remove active class from all buttons
|
74
|
+
this.amountButtonTargets.forEach(btn => {
|
75
|
+
btn.classList.remove("tipjar-amount-active")
|
76
|
+
})
|
77
|
+
|
78
|
+
// Add active class to selected button
|
79
|
+
button.classList.add("tipjar-amount-active")
|
80
|
+
|
81
|
+
this.selectedAmount = amount
|
82
|
+
|
83
|
+
if (this.analyticsEnabledValue) {
|
84
|
+
this.trackEvent("tipjar_amount_selected", { amount })
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
submitTip(event) {
|
89
|
+
event.preventDefault()
|
90
|
+
|
91
|
+
if (!this.selectedAmount) {
|
92
|
+
// Find default amount or select first one
|
93
|
+
const defaultButton = this.amountButtonTargets.find(btn =>
|
94
|
+
btn.dataset.default === "true"
|
95
|
+
) || this.amountButtonTargets[0]
|
96
|
+
|
97
|
+
if (defaultButton) {
|
98
|
+
this.selectedAmount = defaultButton.dataset.amount
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
const paymentLink = this.getPaymentLink(this.selectedAmount)
|
103
|
+
|
104
|
+
if (paymentLink) {
|
105
|
+
if (this.analyticsEnabledValue) {
|
106
|
+
this.trackEvent("tipjar_submitted", {
|
107
|
+
amount: this.selectedAmount,
|
108
|
+
url: paymentLink
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
window.open(paymentLink, "_blank", "noopener,noreferrer")
|
113
|
+
this.closeModal()
|
114
|
+
} else {
|
115
|
+
console.error("No payment link found for amount:", this.selectedAmount)
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
getPaymentLink(amount) {
|
120
|
+
// Check if amount is a key in payment links
|
121
|
+
if (this.paymentLinksValue[amount]) {
|
122
|
+
return this.paymentLinksValue[amount]
|
123
|
+
}
|
124
|
+
|
125
|
+
// Try to match by dollar amount
|
126
|
+
const numericAmount = parseInt(amount)
|
127
|
+
if (this.paymentLinksValue[`amount_${numericAmount}`]) {
|
128
|
+
return this.paymentLinksValue[`amount_${numericAmount}`]
|
129
|
+
}
|
130
|
+
|
131
|
+
// Fallback to medium or first available link
|
132
|
+
return this.paymentLinksValue.medium ||
|
133
|
+
this.paymentLinksValue.default ||
|
134
|
+
Object.values(this.paymentLinksValue)[0]
|
135
|
+
}
|
136
|
+
|
137
|
+
trackEvent(eventName, data = {}) {
|
138
|
+
// Google Analytics 4
|
139
|
+
if (typeof gtag !== "undefined") {
|
140
|
+
gtag("event", eventName, data)
|
141
|
+
}
|
142
|
+
|
143
|
+
// Plausible
|
144
|
+
if (typeof plausible !== "undefined") {
|
145
|
+
plausible(eventName, { props: data })
|
146
|
+
}
|
147
|
+
|
148
|
+
// Custom tracking
|
149
|
+
const event = new CustomEvent("tipjar:event", {
|
150
|
+
detail: { name: eventName, ...data }
|
151
|
+
})
|
152
|
+
document.dispatchEvent(event)
|
153
|
+
}
|
154
|
+
}
|
@@ -0,0 +1,213 @@
|
|
1
|
+
/* Tip Jar Button Styles */
|
2
|
+
.tipjar-button {
|
3
|
+
display: flex;
|
4
|
+
align-items: center;
|
5
|
+
gap: 0.5rem;
|
6
|
+
padding: 0.75rem 1rem;
|
7
|
+
background-color: #3b82f6;
|
8
|
+
color: white;
|
9
|
+
border-radius: 9999px;
|
10
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
11
|
+
transition: all 0.3s ease;
|
12
|
+
cursor: pointer;
|
13
|
+
border: none;
|
14
|
+
font-size: 0.875rem;
|
15
|
+
font-weight: 500;
|
16
|
+
animation: tipjar-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
17
|
+
}
|
18
|
+
|
19
|
+
.tipjar-button:hover {
|
20
|
+
background-color: #2563eb;
|
21
|
+
transform: scale(1.05);
|
22
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
23
|
+
}
|
24
|
+
|
25
|
+
.tipjar-button:focus {
|
26
|
+
outline: 2px solid transparent;
|
27
|
+
outline-offset: 2px;
|
28
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
29
|
+
}
|
30
|
+
|
31
|
+
.tipjar-button-icon {
|
32
|
+
display: flex;
|
33
|
+
align-items: center;
|
34
|
+
justify-content: center;
|
35
|
+
width: 1.5rem;
|
36
|
+
height: 1.5rem;
|
37
|
+
}
|
38
|
+
|
39
|
+
.tipjar-button-text {
|
40
|
+
white-space: nowrap;
|
41
|
+
max-width: 0;
|
42
|
+
overflow: hidden;
|
43
|
+
transition: max-width 0.3s ease;
|
44
|
+
}
|
45
|
+
|
46
|
+
.tipjar-button:hover .tipjar-button-text {
|
47
|
+
max-width: 200px;
|
48
|
+
margin-left: 0.25rem;
|
49
|
+
}
|
50
|
+
|
51
|
+
/* Animation */
|
52
|
+
@keyframes tipjar-pulse {
|
53
|
+
0%, 100% {
|
54
|
+
opacity: 1;
|
55
|
+
}
|
56
|
+
50% {
|
57
|
+
opacity: 0.8;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
/* Coin falling animation */
|
62
|
+
.tipjar-coin-animation {
|
63
|
+
animation: tipjar-coin-drop 2s ease-in-out infinite;
|
64
|
+
transform-origin: center;
|
65
|
+
}
|
66
|
+
|
67
|
+
@keyframes tipjar-coin-drop {
|
68
|
+
0% {
|
69
|
+
transform: translateY(0) rotate(0deg);
|
70
|
+
opacity: 0;
|
71
|
+
}
|
72
|
+
20% {
|
73
|
+
opacity: 1;
|
74
|
+
}
|
75
|
+
80% {
|
76
|
+
transform: translateY(7px) rotate(180deg);
|
77
|
+
opacity: 1;
|
78
|
+
}
|
79
|
+
100% {
|
80
|
+
transform: translateY(9px) rotate(180deg);
|
81
|
+
opacity: 0;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
/* Modal Styles */
|
86
|
+
.tipjar-modal {
|
87
|
+
z-index: 9999;
|
88
|
+
}
|
89
|
+
|
90
|
+
.tipjar-modal-backdrop {
|
91
|
+
animation: tipjar-fade-in 0.2s ease-out;
|
92
|
+
}
|
93
|
+
|
94
|
+
.tipjar-modal-content {
|
95
|
+
animation: tipjar-slide-up 0.3s ease-out;
|
96
|
+
}
|
97
|
+
|
98
|
+
@keyframes tipjar-fade-in {
|
99
|
+
from {
|
100
|
+
opacity: 0;
|
101
|
+
}
|
102
|
+
to {
|
103
|
+
opacity: 1;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
@keyframes tipjar-slide-up {
|
108
|
+
from {
|
109
|
+
opacity: 0;
|
110
|
+
transform: translateY(1rem);
|
111
|
+
}
|
112
|
+
to {
|
113
|
+
opacity: 1;
|
114
|
+
transform: translateY(0);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
/* Amount Button Styles */
|
119
|
+
.tipjar-amount-button {
|
120
|
+
border: 2px solid #e5e7eb;
|
121
|
+
background-color: white;
|
122
|
+
color: #374151;
|
123
|
+
font-weight: 500;
|
124
|
+
}
|
125
|
+
|
126
|
+
.tipjar-amount-button:hover {
|
127
|
+
border-color: #3b82f6;
|
128
|
+
background-color: #eff6ff;
|
129
|
+
}
|
130
|
+
|
131
|
+
.tipjar-amount-button.tipjar-amount-active {
|
132
|
+
border-color: #3b82f6;
|
133
|
+
background-color: #3b82f6;
|
134
|
+
color: white;
|
135
|
+
}
|
136
|
+
|
137
|
+
/* Dark Theme Support */
|
138
|
+
@media (prefers-color-scheme: dark) {
|
139
|
+
.tipjar-modal-content {
|
140
|
+
background-color: #1f2937;
|
141
|
+
color: #f3f4f6;
|
142
|
+
}
|
143
|
+
|
144
|
+
.tipjar-modal-header h3 {
|
145
|
+
color: #f3f4f6;
|
146
|
+
}
|
147
|
+
|
148
|
+
.tipjar-modal-header p {
|
149
|
+
color: #9ca3af;
|
150
|
+
}
|
151
|
+
|
152
|
+
.tipjar-amount-button {
|
153
|
+
background-color: #374151;
|
154
|
+
border-color: #4b5563;
|
155
|
+
color: #f3f4f6;
|
156
|
+
}
|
157
|
+
|
158
|
+
.tipjar-amount-button:hover {
|
159
|
+
background-color: #4b5563;
|
160
|
+
border-color: #60a5fa;
|
161
|
+
}
|
162
|
+
|
163
|
+
.tipjar-modal-close {
|
164
|
+
color: #9ca3af;
|
165
|
+
}
|
166
|
+
|
167
|
+
.tipjar-modal-close:hover {
|
168
|
+
color: #f3f4f6;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
/* Responsive Design */
|
173
|
+
@media (max-width: 640px) {
|
174
|
+
.tipjar-button {
|
175
|
+
padding: 0.625rem 0.875rem;
|
176
|
+
font-size: 0.75rem;
|
177
|
+
}
|
178
|
+
|
179
|
+
.tipjar-button-icon {
|
180
|
+
width: 1.25rem;
|
181
|
+
height: 1.25rem;
|
182
|
+
}
|
183
|
+
|
184
|
+
.tipjar-modal-content {
|
185
|
+
margin: 1rem;
|
186
|
+
max-width: calc(100% - 2rem);
|
187
|
+
}
|
188
|
+
|
189
|
+
.tipjar-modal-amounts .grid {
|
190
|
+
grid-template-columns: 1fr;
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
/* Position Classes */
|
195
|
+
.fixed {
|
196
|
+
position: fixed;
|
197
|
+
}
|
198
|
+
|
199
|
+
.bottom-4 {
|
200
|
+
bottom: 1rem;
|
201
|
+
}
|
202
|
+
|
203
|
+
.right-4 {
|
204
|
+
right: 1rem;
|
205
|
+
}
|
206
|
+
|
207
|
+
.left-4 {
|
208
|
+
left: 1rem;
|
209
|
+
}
|
210
|
+
|
211
|
+
.top-4 {
|
212
|
+
top: 1rem;
|
213
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module TipjarHelper
|
2
|
+
def tipjar_button(options = {})
|
3
|
+
config = RailsTipjar.config
|
4
|
+
|
5
|
+
position = options[:position] || config.position
|
6
|
+
custom_class = options[:class] || config.button_class
|
7
|
+
z_index = options[:z_index] || config.z_index
|
8
|
+
|
9
|
+
# Simple direct link mode when payment_link is set
|
10
|
+
if config.payment_link && !config.use_modal
|
11
|
+
link_to config.payment_link,
|
12
|
+
target: "_blank",
|
13
|
+
rel: "noopener",
|
14
|
+
class: "tipjar-button #{config_position_classes(position)} #{custom_class}",
|
15
|
+
style: "z-index: #{z_index};" do
|
16
|
+
content_tag(:span, class: "tipjar-button-icon") do
|
17
|
+
raw config.icon_svg
|
18
|
+
end +
|
19
|
+
content_tag(:span, config.button_text, class: "tipjar-button-text")
|
20
|
+
end
|
21
|
+
else
|
22
|
+
# Original modal mode
|
23
|
+
content_tag :div,
|
24
|
+
class: "tipjar-container",
|
25
|
+
data: {
|
26
|
+
controller: "tipjar",
|
27
|
+
tipjar_payment_links_value: config.payment_links.to_json,
|
28
|
+
tipjar_theme_value: config.theme,
|
29
|
+
tipjar_modal_title_value: config.modal_title,
|
30
|
+
tipjar_modal_description_value: config.modal_description,
|
31
|
+
tipjar_custom_amounts_value: config.custom_amounts.to_json,
|
32
|
+
tipjar_analytics_enabled_value: config.analytics_enabled
|
33
|
+
} do
|
34
|
+
render partial: "tipjar/button", locals: {
|
35
|
+
position: position,
|
36
|
+
custom_class: custom_class,
|
37
|
+
z_index: z_index,
|
38
|
+
config: config
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def tipjar_link(text = nil, options = {})
|
45
|
+
config = RailsTipjar.config
|
46
|
+
text ||= config.button_text
|
47
|
+
amount = options[:amount] || :medium
|
48
|
+
|
49
|
+
link_url = if amount.is_a?(Symbol)
|
50
|
+
config.payment_links[amount]
|
51
|
+
else
|
52
|
+
config.payment_links[:custom] || config.payment_links[:medium]
|
53
|
+
end
|
54
|
+
|
55
|
+
link_options = {
|
56
|
+
target: "_blank",
|
57
|
+
rel: "noopener",
|
58
|
+
class: "tipjar-link #{options[:class]}",
|
59
|
+
data: {
|
60
|
+
tipjar_amount: amount,
|
61
|
+
tipjar_analytics: config.analytics_enabled
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
link_to text, link_url, link_options.merge(options.except(:amount, :class))
|
66
|
+
end
|
67
|
+
|
68
|
+
def tipjar_modal(options = {})
|
69
|
+
config = RailsTipjar.config
|
70
|
+
|
71
|
+
render partial: "tipjar/modal", locals: {
|
72
|
+
config: config,
|
73
|
+
options: options
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def config_position_classes(position)
|
80
|
+
case position
|
81
|
+
when :bottom_right
|
82
|
+
"fixed bottom-4 right-4"
|
83
|
+
when :bottom_left
|
84
|
+
"fixed bottom-4 left-4"
|
85
|
+
when :top_right
|
86
|
+
"fixed top-4 right-4"
|
87
|
+
when :top_left
|
88
|
+
"fixed top-4 left-4"
|
89
|
+
else
|
90
|
+
"fixed bottom-4 right-4"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<button
|
2
|
+
data-tipjar-target="button"
|
3
|
+
data-action="click->tipjar#openModal"
|
4
|
+
class="tipjar-button <%= config_position_classes(position) %> <%= custom_class %>"
|
5
|
+
style="z-index: <%= z_index %>;"
|
6
|
+
aria-label="<%= config.button_text %>"
|
7
|
+
>
|
8
|
+
<span class="tipjar-button-icon">
|
9
|
+
<%= raw config.icon_svg %>
|
10
|
+
</span>
|
11
|
+
<span class="tipjar-button-text">
|
12
|
+
<%= config.button_text %>
|
13
|
+
</span>
|
14
|
+
</button>
|
15
|
+
|
16
|
+
<%= render "tipjar/modal", config: config %>
|
@@ -0,0 +1,74 @@
|
|
1
|
+
<div
|
2
|
+
data-tipjar-target="modal"
|
3
|
+
class="tipjar-modal hidden fixed inset-0 z-50 overflow-y-auto"
|
4
|
+
aria-labelledby="tipjar-modal-title"
|
5
|
+
role="dialog"
|
6
|
+
aria-modal="true"
|
7
|
+
>
|
8
|
+
<!-- Backdrop -->
|
9
|
+
<div
|
10
|
+
data-tipjar-target="backdrop"
|
11
|
+
class="tipjar-modal-backdrop fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
12
|
+
></div>
|
13
|
+
|
14
|
+
<!-- Modal Content -->
|
15
|
+
<div class="flex items-center justify-center min-h-screen p-4">
|
16
|
+
<div class="tipjar-modal-content relative bg-white rounded-lg max-w-md w-full p-6 shadow-xl">
|
17
|
+
<!-- Close button -->
|
18
|
+
<button
|
19
|
+
type="button"
|
20
|
+
data-action="click->tipjar#closeModal"
|
21
|
+
class="tipjar-modal-close absolute top-4 right-4 text-gray-400 hover:text-gray-500"
|
22
|
+
aria-label="Close modal"
|
23
|
+
>
|
24
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
25
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
26
|
+
</svg>
|
27
|
+
</button>
|
28
|
+
|
29
|
+
<!-- Modal Header -->
|
30
|
+
<div class="tipjar-modal-header mb-4">
|
31
|
+
<h3 id="tipjar-modal-title" class="text-lg font-semibold text-gray-900">
|
32
|
+
<%= config.modal_title %>
|
33
|
+
</h3>
|
34
|
+
<p class="mt-2 text-sm text-gray-600">
|
35
|
+
<%= config.modal_description %>
|
36
|
+
</p>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<!-- Amount Selection -->
|
40
|
+
<div class="tipjar-modal-amounts mb-6">
|
41
|
+
<div class="grid grid-cols-2 gap-3">
|
42
|
+
<% config.custom_amounts.each do |amount_option| %>
|
43
|
+
<button
|
44
|
+
type="button"
|
45
|
+
data-tipjar-target="amountButton"
|
46
|
+
data-action="click->tipjar#selectAmount"
|
47
|
+
data-amount="<%= amount_option[:amount] %>"
|
48
|
+
data-default="<%= amount_option[:default] %>"
|
49
|
+
class="tipjar-amount-button px-4 py-3 border rounded-lg text-center transition-colors hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 <%= 'tipjar-amount-active' if amount_option[:default] %>"
|
50
|
+
>
|
51
|
+
<%= amount_option[:label] %>
|
52
|
+
</button>
|
53
|
+
<% end %>
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
<!-- Submit Button -->
|
58
|
+
<button
|
59
|
+
type="button"
|
60
|
+
data-action="click->tipjar#submitTip"
|
61
|
+
class="tipjar-submit-button w-full px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
62
|
+
>
|
63
|
+
Continue to Payment
|
64
|
+
</button>
|
65
|
+
|
66
|
+
<!-- Powered by Stripe -->
|
67
|
+
<div class="tipjar-modal-footer mt-4 text-center">
|
68
|
+
<p class="text-xs text-gray-500">
|
69
|
+
Secure payment powered by Stripe
|
70
|
+
</p>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
</div>
|
data/config/importmap.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pin "tipjar_controller", to: "tipjar_controller.js"
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "rails/generators/base"
|
2
|
+
|
3
|
+
module Tipjar
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
desc "Creates a RailsTipjar initializer and copies necessary files"
|
9
|
+
|
10
|
+
def copy_initializer
|
11
|
+
template "tipjar.rb", "config/initializers/tipjar.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_stimulus_controller
|
15
|
+
if File.exist?("app/javascript/controllers/index.js")
|
16
|
+
append_to_file "app/javascript/controllers/index.js" do
|
17
|
+
<<~JS
|
18
|
+
|
19
|
+
// Register Tip Jar controller
|
20
|
+
import TipjarController from "tipjar_controller"
|
21
|
+
application.register("tipjar", TipjarController)
|
22
|
+
JS
|
23
|
+
end
|
24
|
+
else
|
25
|
+
say "Please manually register the tipjar Stimulus controller in your application", :yellow
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_stylesheet
|
30
|
+
if File.exist?("app/assets/stylesheets/application.css")
|
31
|
+
append_to_file "app/assets/stylesheets/application.css" do
|
32
|
+
<<~CSS
|
33
|
+
|
34
|
+
/* Tip Jar Styles */
|
35
|
+
@import 'tipjar';
|
36
|
+
CSS
|
37
|
+
end
|
38
|
+
elsif File.exist?("app/assets/stylesheets/application.scss")
|
39
|
+
append_to_file "app/assets/stylesheets/application.scss" do
|
40
|
+
<<~SCSS
|
41
|
+
|
42
|
+
// Tip Jar Styles
|
43
|
+
@import 'tipjar';
|
44
|
+
SCSS
|
45
|
+
end
|
46
|
+
elsif File.exist?("app/assets/stylesheets/application.tailwind.css")
|
47
|
+
append_to_file "app/assets/stylesheets/application.tailwind.css" do
|
48
|
+
<<~CSS
|
49
|
+
|
50
|
+
/* Tip Jar Styles */
|
51
|
+
@import 'tipjar';
|
52
|
+
CSS
|
53
|
+
end
|
54
|
+
else
|
55
|
+
say "Please manually import 'tipjar' stylesheet in your application", :yellow
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_helper_to_application_controller
|
60
|
+
inject_into_file "app/controllers/application_controller.rb",
|
61
|
+
after: "class ApplicationController < ActionController::Base\n" do
|
62
|
+
" helper TipjarHelper\n"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def display_post_install_message
|
67
|
+
say "\n", :green
|
68
|
+
say "RailsTipjar has been successfully installed!", :green
|
69
|
+
say "\n"
|
70
|
+
say "Next steps:", :yellow
|
71
|
+
say "1. Create Stripe Payment Links at https://dashboard.stripe.com/payment-links", :yellow
|
72
|
+
say "2. Configure your payment links in config/initializers/tipjar.rb", :yellow
|
73
|
+
say "3. Add <%= tipjar_button %> to your layout file", :yellow
|
74
|
+
say "\n"
|
75
|
+
say "Example configuration:", :cyan
|
76
|
+
say <<~CONFIG
|
77
|
+
RailsTipjar.configure do |config|
|
78
|
+
config.payment_links = {
|
79
|
+
small: "https://buy.stripe.com/your-link-1",
|
80
|
+
medium: "https://buy.stripe.com/your-link-2",
|
81
|
+
large: "https://buy.stripe.com/your-link-3"
|
82
|
+
}
|
83
|
+
config.position = :bottom_right
|
84
|
+
config.icon = :coffee
|
85
|
+
config.theme = :light
|
86
|
+
end
|
87
|
+
CONFIG
|
88
|
+
say "\n"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RailsTipjar.configure do |config|
|
4
|
+
# Configure your Stripe Payment Links
|
5
|
+
# Create payment links at: https://dashboard.stripe.com/payment-links
|
6
|
+
#
|
7
|
+
# You can create different links for different amounts, or use a single
|
8
|
+
# link that allows custom amounts
|
9
|
+
config.payment_links = {
|
10
|
+
small: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
|
11
|
+
medium: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
|
12
|
+
large: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
|
13
|
+
custom: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK"
|
14
|
+
}
|
15
|
+
|
16
|
+
# Position of the tip jar button
|
17
|
+
# Options: :bottom_right, :bottom_left, :top_right, :top_left
|
18
|
+
config.position = :bottom_right
|
19
|
+
|
20
|
+
# Icon to display in the button
|
21
|
+
# Options: :coffee, :heart, :star, :dollar, or custom SVG string
|
22
|
+
config.icon = :coffee
|
23
|
+
|
24
|
+
# Theme for the modal
|
25
|
+
# Options: :light, :dark, :auto
|
26
|
+
config.theme = :light
|
27
|
+
|
28
|
+
# Button text (shown on hover)
|
29
|
+
config.button_text = "Buy me a coffee"
|
30
|
+
|
31
|
+
# Modal customization
|
32
|
+
config.modal_title = "Support my work"
|
33
|
+
config.modal_description = "Your support helps me continue creating and maintaining this project."
|
34
|
+
|
35
|
+
# Custom tip amounts to display in the modal
|
36
|
+
config.custom_amounts = [
|
37
|
+
{ amount: 5, label: "$5", default: false },
|
38
|
+
{ amount: 10, label: "$10", default: true },
|
39
|
+
{ amount: 25, label: "$25", default: false },
|
40
|
+
{ amount: 50, label: "$50", default: false }
|
41
|
+
]
|
42
|
+
|
43
|
+
# Additional CSS classes for the button
|
44
|
+
config.button_class = ""
|
45
|
+
|
46
|
+
# Z-index for the button and modal
|
47
|
+
config.z_index = 1000
|
48
|
+
|
49
|
+
# Enable analytics tracking
|
50
|
+
# Tracks: button clicks, modal opens, amount selections, and successful tips
|
51
|
+
# Works with Google Analytics 4 and Plausible out of the box
|
52
|
+
config.analytics_enabled = false
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module RailsTipjar
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :payment_links, :position, :icon, :theme, :button_text,
|
4
|
+
:modal_title, :modal_description, :custom_amounts,
|
5
|
+
:button_class, :z_index, :analytics_enabled, :payment_link,
|
6
|
+
:use_modal
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
# Simple mode - just one link
|
10
|
+
@payment_link = nil
|
11
|
+
@use_modal = false # Default to direct link when payment_link is set
|
12
|
+
|
13
|
+
# Legacy modal mode
|
14
|
+
@payment_links = {}
|
15
|
+
@position = :bottom_right
|
16
|
+
@icon = :coffee
|
17
|
+
@theme = :light
|
18
|
+
@button_text = "Buy me a coffee"
|
19
|
+
@modal_title = "Support my work"
|
20
|
+
@modal_description = "Your support helps me continue creating and maintaining this project."
|
21
|
+
@custom_amounts = [
|
22
|
+
{ amount: 5, label: "$5", default: false },
|
23
|
+
{ amount: 10, label: "$10", default: true },
|
24
|
+
{ amount: 25, label: "$25", default: false },
|
25
|
+
{ amount: 50, label: "$50", default: false }
|
26
|
+
]
|
27
|
+
@button_class = ""
|
28
|
+
@z_index = 1000
|
29
|
+
@analytics_enabled = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def position_classes
|
33
|
+
case @position
|
34
|
+
when :bottom_right
|
35
|
+
"bottom-4 right-4"
|
36
|
+
when :bottom_left
|
37
|
+
"bottom-4 left-4"
|
38
|
+
when :top_right
|
39
|
+
"top-4 right-4"
|
40
|
+
when :top_left
|
41
|
+
"top-4 left-4"
|
42
|
+
else
|
43
|
+
"bottom-4 right-4"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def icon_svg
|
48
|
+
case @icon
|
49
|
+
when :coffee
|
50
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25v7.5c0 2.485 2.099 4.5 4.688 4.5 1.935 0 3.597-1.126 4.313-2.733.715 1.607 2.377 2.733 4.313 2.733 2.588 0 4.687-2.015 4.687-4.5v-7.5z" />
|
52
|
+
</svg>'
|
53
|
+
when :heart
|
54
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.5c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 4 3 6.015 3 8.5c0 7.5 9 11.5 9 11.5s9-4 9-11.5z" />
|
56
|
+
</svg>'
|
57
|
+
when :star
|
58
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
|
60
|
+
</svg>'
|
61
|
+
when :dollar
|
62
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
63
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
64
|
+
</svg>'
|
65
|
+
when :jar
|
66
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
67
|
+
<!-- Jar lid -->
|
68
|
+
<rect x="8.5" y="3" width="7" height="1.5" rx="0.25" stroke-linecap="round" stroke-linejoin="round"/>
|
69
|
+
<!-- Jar threads/neck -->
|
70
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 4.5v1.5M15 4.5v1.5"/>
|
71
|
+
<!-- Jar body with taper -->
|
72
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6c0 0 -1 0.5 -1 1.5v11.5a2 2 0 002 2h4a2 2 0 002-2V7.5c0-1 -1-1.5 -1-1.5"/>
|
73
|
+
<!-- Coins at bottom -->
|
74
|
+
<ellipse cx="10.5" cy="18.5" rx="1.5" ry="0.5" stroke-linecap="round"/>
|
75
|
+
<ellipse cx="14" cy="18" rx="1.5" ry="0.5" stroke-linecap="round"/>
|
76
|
+
<ellipse cx="12" cy="17.5" rx="1.5" ry="0.5" stroke-linecap="round"/>
|
77
|
+
<!-- Falling coin -->
|
78
|
+
<g class="tipjar-coin-animation">
|
79
|
+
<ellipse cx="12" cy="10" rx="1.5" ry="0.5" stroke-linecap="round"/>
|
80
|
+
<path stroke-linecap="round" d="M12 10v1.5"/>
|
81
|
+
</g>
|
82
|
+
</svg>'
|
83
|
+
else
|
84
|
+
@icon
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module RailsTipjar
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace RailsTipjar
|
4
|
+
|
5
|
+
initializer "rails_tipjar.assets" do |app|
|
6
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
7
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
8
|
+
app.config.assets.precompile += %w[
|
9
|
+
tipjar_controller.js
|
10
|
+
tipjar.css
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer "rails_tipjar.helpers" do
|
15
|
+
ActiveSupport.on_load(:action_controller_base) do
|
16
|
+
helper RailsTipjar::Engine.helpers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "rails_tipjar.importmap", before: "importmap" do |app|
|
21
|
+
if defined?(Importmap)
|
22
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/rails_tipjar.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rails_tipjar/version"
|
4
|
+
require_relative "rails_tipjar/configuration"
|
5
|
+
require_relative "rails_tipjar/engine" if defined?(Rails)
|
6
|
+
|
7
|
+
module RailsTipjar
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
self.configuration ||= Configuration.new
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config
|
20
|
+
self.configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_tipjar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Paulson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: stimulus-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
description: Easily add a customizable tip jar to any Rails application using Stripe
|
42
|
+
Payment Links
|
43
|
+
email:
|
44
|
+
- justinapaulson@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- app/assets/javascripts/tipjar_controller.js
|
52
|
+
- app/assets/stylesheets/tipjar.css
|
53
|
+
- app/helpers/tipjar_helper.rb
|
54
|
+
- app/views/tipjar/_button.html.erb
|
55
|
+
- app/views/tipjar/_modal.html.erb
|
56
|
+
- config/importmap.rb
|
57
|
+
- lib/generators/tipjar/install/install_generator.rb
|
58
|
+
- lib/generators/tipjar/install/templates/tipjar.rb
|
59
|
+
- lib/rails_tipjar.rb
|
60
|
+
- lib/rails_tipjar/configuration.rb
|
61
|
+
- lib/rails_tipjar/engine.rb
|
62
|
+
- lib/rails_tipjar/version.rb
|
63
|
+
- sig/rails_tipjar.rbs
|
64
|
+
homepage: https://github.com/justinpaulson/rails_tipjar
|
65
|
+
licenses: []
|
66
|
+
metadata:
|
67
|
+
homepage_uri: https://github.com/justinpaulson/rails_tipjar
|
68
|
+
source_code_uri: https://github.com/justinpaulson/rails_tipjar
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.0.0
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.5.11
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: A reusable tip jar feature for Rails applications
|
88
|
+
test_files: []
|