mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-21 20:09:57 -06:00
@@ -80,6 +80,7 @@ from .link_template import LinkTemplate
|
||||
from .custom_field_definition import CustomFieldDefinition
|
||||
from .salesman_email_mapping import SalesmanEmailMapping
|
||||
from .issue import Issue
|
||||
from .donation_interaction import DonationInteraction
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -185,4 +186,5 @@ __all__ = [
|
||||
"CustomFieldDefinition",
|
||||
"SalesmanEmailMapping",
|
||||
"Issue",
|
||||
"DonationInteraction",
|
||||
]
|
||||
|
||||
98
app/models/donation_interaction.py
Normal file
98
app/models/donation_interaction.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Model to track donation banner interactions and user engagement metrics"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from app import db
|
||||
|
||||
|
||||
class DonationInteraction(db.Model):
|
||||
"""Track user interactions with donation prompts"""
|
||||
|
||||
__tablename__ = "donation_interactions"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# Interaction type
|
||||
interaction_type = db.Column(
|
||||
db.String(50), nullable=False
|
||||
) # 'banner_dismissed', 'banner_clicked', 'link_clicked', 'page_viewed'
|
||||
|
||||
# Context
|
||||
source = db.Column(db.String(100), nullable=True) # 'dashboard', 'banner', 'menu', 'footer', etc.
|
||||
|
||||
# User metrics at time of interaction (for smart prompts)
|
||||
time_entries_count = db.Column(db.Integer, nullable=True) # Total time entries
|
||||
days_since_signup = db.Column(db.Integer, nullable=True) # Days since user created account
|
||||
total_hours = db.Column(db.Float, nullable=True) # Total hours tracked
|
||||
|
||||
# Timestamps
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = db.relationship("User", backref="donation_interactions")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DonationInteraction {self.interaction_type} by user {self.user_id}>"
|
||||
|
||||
@staticmethod
|
||||
def record_interaction(user_id: int, interaction_type: str, source: str = None, user_metrics: dict = None):
|
||||
"""Record a donation interaction"""
|
||||
interaction = DonationInteraction(
|
||||
user_id=user_id,
|
||||
interaction_type=interaction_type,
|
||||
source=source,
|
||||
)
|
||||
|
||||
if user_metrics:
|
||||
interaction.time_entries_count = user_metrics.get("time_entries_count")
|
||||
interaction.days_since_signup = user_metrics.get("days_since_signup")
|
||||
interaction.total_hours = user_metrics.get("total_hours")
|
||||
|
||||
db.session.add(interaction)
|
||||
db.session.commit()
|
||||
return interaction
|
||||
|
||||
@staticmethod
|
||||
def has_recent_donation_click(user_id: int, days: int = 30) -> bool:
|
||||
"""Check if user clicked donation link in last N days"""
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
return (
|
||||
DonationInteraction.query.filter_by(
|
||||
user_id=user_id, interaction_type="banner_clicked"
|
||||
)
|
||||
.filter(DonationInteraction.created_at >= cutoff)
|
||||
.first()
|
||||
is not None
|
||||
) or (
|
||||
DonationInteraction.query.filter_by(
|
||||
user_id=user_id, interaction_type="link_clicked"
|
||||
)
|
||||
.filter(DonationInteraction.created_at >= cutoff)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_user_engagement_metrics(user_id: int) -> dict:
|
||||
"""Get user engagement metrics for smart prompts"""
|
||||
from app.models import TimeEntry, User
|
||||
|
||||
user = User.query.get(user_id)
|
||||
if not user:
|
||||
return {}
|
||||
|
||||
# Days since signup
|
||||
days_since_signup = (datetime.utcnow() - user.created_at).days if user.created_at else 0
|
||||
|
||||
# Time entries count
|
||||
time_entries_count = TimeEntry.query.filter_by(user_id=user_id).count()
|
||||
|
||||
# Total hours
|
||||
total_hours = user.total_hours if hasattr(user, "total_hours") else 0.0
|
||||
|
||||
return {
|
||||
"days_since_signup": days_since_signup,
|
||||
"time_entries_count": time_entries_count,
|
||||
"total_hours": total_hours,
|
||||
}
|
||||
|
||||
@@ -105,6 +105,25 @@ def dashboard():
|
||||
# Get recent activities for activity feed widget
|
||||
recent_activities = Activity.get_recent(user_id=None if current_user.is_admin else current_user.id, limit=10)
|
||||
|
||||
# Get user stats for smart banner and donation widget
|
||||
try:
|
||||
from app.models import DonationInteraction
|
||||
user_stats = DonationInteraction.get_user_engagement_metrics(current_user.id)
|
||||
except Exception:
|
||||
# Fallback if table doesn't exist yet
|
||||
days_since_signup = (datetime.utcnow() - current_user.created_at).days if current_user.created_at else 0
|
||||
time_entries_count = TimeEntry.query.filter_by(user_id=current_user.id).count()
|
||||
total_hours = current_user.total_hours if hasattr(current_user, "total_hours") else 0.0
|
||||
user_stats = {
|
||||
"days_since_signup": days_since_signup,
|
||||
"time_entries_count": time_entries_count,
|
||||
"total_hours": total_hours,
|
||||
}
|
||||
|
||||
# Get donation widget stats (separate from user_stats for clarity)
|
||||
time_entries_count = user_stats.get("time_entries_count", 0)
|
||||
total_hours = user_stats.get("total_hours", 0.0)
|
||||
|
||||
# Prepare template data
|
||||
template_data = {
|
||||
"active_timer": active_timer,
|
||||
@@ -118,6 +137,9 @@ def dashboard():
|
||||
"current_week_goal": current_week_goal,
|
||||
"templates": templates,
|
||||
"recent_activities": recent_activities,
|
||||
"user_stats": user_stats, # For smart banner
|
||||
"time_entries_count": time_entries_count, # For donation widget
|
||||
"total_hours": total_hours, # For donation widget
|
||||
}
|
||||
|
||||
# Cache for 5 minutes
|
||||
@@ -154,6 +176,93 @@ def help():
|
||||
return render_template("main/help.html")
|
||||
|
||||
|
||||
@main_bp.route("/donate")
|
||||
@login_required
|
||||
def donate():
|
||||
"""Donation page explaining why donations are important"""
|
||||
from app.models import TimeEntry
|
||||
|
||||
# Get user engagement metrics
|
||||
days_since_signup = (datetime.utcnow() - current_user.created_at).days if current_user.created_at else 0
|
||||
time_entries_count = TimeEntry.query.filter_by(user_id=current_user.id).count()
|
||||
total_hours = current_user.total_hours if hasattr(current_user, "total_hours") else 0.0
|
||||
|
||||
# Record page view (only if table exists)
|
||||
try:
|
||||
from app.models import DonationInteraction
|
||||
DonationInteraction.record_interaction(
|
||||
user_id=current_user.id,
|
||||
interaction_type="page_viewed",
|
||||
source="donate_page",
|
||||
user_metrics={
|
||||
"days_since_signup": days_since_signup,
|
||||
"time_entries_count": time_entries_count,
|
||||
"total_hours": total_hours,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if tracking fails (e.g., table doesn't exist yet)
|
||||
pass
|
||||
|
||||
return render_template(
|
||||
"main/donate.html",
|
||||
days_since_signup=days_since_signup,
|
||||
time_entries_count=time_entries_count,
|
||||
total_hours=total_hours,
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/donate/track-click", methods=["POST"])
|
||||
@login_required
|
||||
def track_donation_click():
|
||||
"""Track donation link clicks"""
|
||||
try:
|
||||
from app.models import DonationInteraction
|
||||
|
||||
data = request.get_json() or {}
|
||||
source = data.get("source", "unknown")
|
||||
|
||||
# Get user metrics
|
||||
metrics = DonationInteraction.get_user_engagement_metrics(current_user.id)
|
||||
|
||||
# Record click
|
||||
DonationInteraction.record_interaction(
|
||||
user_id=current_user.id,
|
||||
interaction_type="link_clicked",
|
||||
source=source,
|
||||
user_metrics=metrics,
|
||||
)
|
||||
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
# Return success even if tracking fails (e.g., table doesn't exist yet)
|
||||
return jsonify({"success": True, "note": "Tracking unavailable"})
|
||||
|
||||
|
||||
@main_bp.route("/donate/track-banner-dismissal", methods=["POST"])
|
||||
@login_required
|
||||
def track_banner_dismissal():
|
||||
"""Track banner dismissals"""
|
||||
try:
|
||||
from app.models import DonationInteraction
|
||||
|
||||
# Get user metrics
|
||||
metrics = DonationInteraction.get_user_engagement_metrics(current_user.id)
|
||||
|
||||
# Record dismissal
|
||||
DonationInteraction.record_interaction(
|
||||
user_id=current_user.id,
|
||||
interaction_type="banner_dismissed",
|
||||
source="banner",
|
||||
user_metrics=metrics,
|
||||
)
|
||||
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
# Return success even if tracking fails (e.g., table doesn't exist yet)
|
||||
return jsonify({"success": True, "note": "Tracking unavailable"})
|
||||
|
||||
|
||||
@main_bp.route("/debug/i18n")
|
||||
@login_required
|
||||
def debug_i18n():
|
||||
|
||||
@@ -865,9 +865,9 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
<a href="https://buymeacoffee.com/DryTrix" target="_blank" rel="noopener noreferrer" class="flex items-center p-2 rounded-lg bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 hover:from-amber-500/20 hover:to-orange-500/20 hover:border-amber-500/30 transition-all duration-200 group">
|
||||
<a href="{{ url_for('main.donate') }}" class="flex items-center p-2 rounded-lg {% if ep == 'main.donate' %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 hover:from-amber-500/20 hover:to-orange-500/20 hover:border-amber-500/30{% endif %} transition-all duration-200 group">
|
||||
<i class="fas fa-mug-saucer w-6 text-center group-hover:scale-110 transition-transform"></i>
|
||||
<span class="ml-3 sidebar-label font-medium">{{ _('Buy me a coffee') }}</span>
|
||||
<span class="ml-3 sidebar-label font-medium">{{ _('Support Development') }}</span>
|
||||
<span class="ml-auto text-xs opacity-70">☕</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -907,9 +907,10 @@
|
||||
<!-- Right side controls -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- BuyMeACoffee Button -->
|
||||
<a href="https://buymeacoffee.com/DryTrix"
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=header&utm_campaign=support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onclick="trackDonationClick('header')"
|
||||
class="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 hover:from-amber-500/20 hover:to-orange-500/20 hover:border-amber-500/30 transition-all duration-200 text-sm font-medium"
|
||||
title="{{ _('Support TimeTracker development') }}">
|
||||
<i class="fas fa-mug-saucer"></i>
|
||||
@@ -972,6 +973,7 @@
|
||||
</li>
|
||||
<li><a href="{{ url_for('auth.profile') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-user w-4"></i> {{ _('My Profile') }}</a></li>
|
||||
<li><a href="{{ url_for('user.settings') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-cog w-4"></i> {{ _('My Settings') }}</a></li>
|
||||
<li><a href="{{ url_for('main.donate') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-amber-600 dark:text-amber-400 hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-mug-saucer w-4"></i> {{ _('Support Development') }}</a></li>
|
||||
<li class="border-t border-border-light dark:border-border-dark"><a href="{{ url_for('auth.logout') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-rose-600 dark:text-rose-400 hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-sign-out-alt w-4"></i> {{ _('Logout') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -995,20 +997,25 @@
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<i class="fas fa-mug-saucer text-amber-600 dark:text-amber-400 text-lg"></i>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-100" id="bannerTitle">
|
||||
{{ _('Enjoying TimeTracker?') }}
|
||||
</p>
|
||||
<p class="text-xs text-amber-700 dark:text-amber-300">
|
||||
{{ _('Support continued development with a coffee') }} ☕
|
||||
<p class="text-xs text-amber-700 dark:text-amber-300" id="bannerMessage">
|
||||
{{ _('Your support helps fund server costs and new features') }} ☕
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="https://buymeacoffee.com/DryTrix"
|
||||
<a href="{{ url_for('main.donate') }}"
|
||||
class="px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
{{ _('Learn More') }}
|
||||
</a>
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=banner&utm_campaign=support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
{{ _('Support') }}
|
||||
onclick="trackDonationClick('banner')"
|
||||
class="px-3 py-1.5 bg-white hover:bg-amber-50 text-amber-600 text-sm font-medium rounded-lg transition-colors border border-amber-600">
|
||||
{{ _('Donate') }}
|
||||
</a>
|
||||
<button onclick="dismissSupportBanner()"
|
||||
class="p-1.5 text-amber-600 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/30 rounded transition-colors"
|
||||
@@ -1023,6 +1030,25 @@
|
||||
<main id="mainContentAnchor" class="flex-1 p-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-card-light dark:bg-card-dark border-t border-border-light dark:border-border-dark py-4 px-6 mt-auto">
|
||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
<div class="flex items-center gap-4 flex-wrap justify-center md:justify-start">
|
||||
<span>© <span id="currentYear"></span> TimeTracker</span>
|
||||
<a href="{{ url_for('main.about') }}" class="hover:text-text-light dark:hover:text-text-dark">{{ _('About') }}</a>
|
||||
<a href="{{ url_for('main.help') }}" class="hover:text-text-light dark:hover:text-text-dark">{{ _('Help') }}</a>
|
||||
<a href="{{ url_for('main.donate') }}" class="text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 font-medium">
|
||||
<i class="fas fa-heart mr-1"></i>{{ _('Support') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ url_for('main.donate') }}" class="px-3 py-1.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white text-sm font-medium rounded-lg transition-all shadow-md hover:shadow-lg">
|
||||
<i class="fas fa-mug-saucer mr-1"></i>{{ _('Donate') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Bottom Navigation -->
|
||||
@@ -1788,31 +1814,115 @@
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAllMenus(); });
|
||||
|
||||
// Support Banner Logic
|
||||
// Support Banner Logic with Smart Prompts
|
||||
function dismissSupportBanner() {
|
||||
const banner = document.getElementById('supportBanner');
|
||||
if (banner) {
|
||||
banner.classList.add('opacity-0', 'invisible', 'max-h-0', 'overflow-hidden');
|
||||
banner.classList.remove('opacity-100', 'visible', 'max-h-[100px]');
|
||||
// Store dismissal timestamp (show again after 7 days)
|
||||
// Store dismissal timestamp (show again after 30 days)
|
||||
try {
|
||||
localStorage.setItem('supportBannerDismissed', Date.now().toString());
|
||||
// Track dismissal
|
||||
trackBannerDismissal();
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function trackBannerDismissal() {
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
fetch('{{ url_for("main.track_banner_dismissal") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({})
|
||||
}).catch(() => {
|
||||
// Silently fail if tracking doesn't work
|
||||
});
|
||||
}
|
||||
|
||||
function trackDonationClick(source) {
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
fetch('{{ url_for("main.track_donation_click") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
source: source
|
||||
})
|
||||
}).catch(() => {
|
||||
// Silently fail if tracking doesn't work
|
||||
});
|
||||
}
|
||||
|
||||
function shouldShowSupportBanner() {
|
||||
try {
|
||||
// Check if dismissed recently (30 days)
|
||||
const dismissed = localStorage.getItem('supportBannerDismissed');
|
||||
if (!dismissed) return true;
|
||||
const dismissedTime = parseInt(dismissed);
|
||||
const sevenDays = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
||||
return (Date.now() - dismissedTime) > sevenDays;
|
||||
if (dismissed) {
|
||||
const dismissedTime = parseInt(dismissed);
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
|
||||
if ((Date.now() - dismissedTime) < thirtyDays) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user clicked donation link recently (30 days)
|
||||
const lastClick = localStorage.getItem('donationLinkClicked');
|
||||
if (lastClick) {
|
||||
const clickTime = parseInt(lastClick);
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if ((Date.now() - clickTime) < thirtyDays) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(e) {
|
||||
return true; // Show by default if localStorage fails
|
||||
}
|
||||
}
|
||||
|
||||
function updateBannerMessage() {
|
||||
// Get user stats from page if available
|
||||
const bannerTitle = document.getElementById('bannerTitle');
|
||||
const bannerMessage = document.getElementById('bannerMessage');
|
||||
|
||||
if (!bannerTitle || !bannerMessage) return;
|
||||
|
||||
// Try to get user stats from data attributes or API
|
||||
const userStats = window.userStats || {};
|
||||
const daysSinceSignup = userStats.days_since_signup || 0;
|
||||
const timeEntriesCount = userStats.time_entries_count || 0;
|
||||
const totalHours = userStats.total_hours || 0;
|
||||
|
||||
// Smart messaging based on milestones
|
||||
if (totalHours >= 100) {
|
||||
bannerTitle.textContent = '{{ _("Amazing! You\'ve tracked over 100 hours") }}';
|
||||
bannerMessage.textContent = '{{ _("Your support helps us keep TimeTracker free and improving") }} ☕';
|
||||
} else if (timeEntriesCount >= 50) {
|
||||
bannerTitle.textContent = '{{ _("Great progress! You\'ve logged 50+ entries") }}';
|
||||
bannerMessage.textContent = '{{ _("Help us continue developing features you love") }} ☕';
|
||||
} else if (daysSinceSignup >= 7) {
|
||||
bannerTitle.textContent = '{{ _("Thanks for using TimeTracker!") }}';
|
||||
bannerMessage.textContent = '{{ _("Your support helps fund server costs and new features") }} ☕';
|
||||
} else {
|
||||
// Default message
|
||||
bannerTitle.textContent = '{{ _("Enjoying TimeTracker?") }}';
|
||||
bannerMessage.textContent = '{{ _("Your support helps fund server costs and new features") }} ☕';
|
||||
}
|
||||
}
|
||||
|
||||
// Show support banner if conditions are met
|
||||
// Check immediately to reserve space and prevent layout shift
|
||||
(function() {
|
||||
@@ -1820,6 +1930,9 @@
|
||||
if (!banner) return;
|
||||
|
||||
if (shouldShowSupportBanner()) {
|
||||
// Update banner message based on user stats
|
||||
updateBannerMessage();
|
||||
|
||||
// Reserve space immediately by removing height constraints
|
||||
// This prevents layout shift when banner becomes visible
|
||||
banner.classList.remove('max-h-0', 'overflow-hidden');
|
||||
@@ -1835,9 +1948,60 @@
|
||||
banner.classList.add('max-h-0', 'overflow-hidden');
|
||||
}
|
||||
})();
|
||||
|
||||
// Track donation link clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
const link = e.target.closest('a[href*="buymeacoffee.com"]');
|
||||
if (link) {
|
||||
try {
|
||||
localStorage.setItem('donationLinkClicked', Date.now().toString());
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='data-tables-enhanced.js') }}"></script>
|
||||
|
||||
<!-- User Stats for Smart Banner -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<script>
|
||||
window.userStats = {
|
||||
days_since_signup: {{ user_stats.days_since_signup if user_stats else 0 }},
|
||||
time_entries_count: {{ user_stats.time_entries_count if user_stats else 0 }},
|
||||
total_hours: {{ user_stats.total_hours if user_stats else 0.0 }}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Set current year in footer -->
|
||||
<script>
|
||||
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<!-- Global donation tracking function -->
|
||||
<script>
|
||||
function trackDonationClick(source) {
|
||||
{% if current_user.is_authenticated %}
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
fetch('{{ url_for("main.track_donation_click") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
source: source
|
||||
})
|
||||
}).catch(() => {
|
||||
// Silently fail if tracking doesn't work
|
||||
});
|
||||
{% endif %}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block scripts_extra %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -175,10 +175,35 @@
|
||||
<a href="https://github.com/drytrix/TimeTracker" target="_blank" rel="noopener" class="px-4 py-2 rounded-lg border border-border-light dark:border-border-dark hover:bg-background-light dark:hover:bg-background-dark">
|
||||
<i class="fab fa-github mr-2"></i>{{ _('View on GitHub') }}
|
||||
</a>
|
||||
<a href="https://buymeacoffee.com/DryTrix" target="_blank" rel="noopener" class="px-4 py-2 rounded-lg border border-amber-600 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20">
|
||||
<i class="fas fa-mug-saucer mr-2"></i>{{ _('Support Development') }}
|
||||
<a href="{{ url_for('main.donate') }}" class="px-4 py-2 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-semibold shadow-md hover:shadow-lg transition-all">
|
||||
<i class="fas fa-heart mr-2"></i>{{ _('Support Development') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Support Section -->
|
||||
<div class="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 p-6 rounded-lg border border-amber-200 dark:border-amber-800 mt-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-mug-saucer text-3xl text-amber-600 dark:text-amber-400"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold mb-2 text-amber-900 dark:text-amber-100">
|
||||
{{ _('Support TimeTracker Development') }}
|
||||
</h3>
|
||||
<p class="text-sm text-amber-800 dark:text-amber-200 mb-4">
|
||||
{{ _('TimeTracker is free and open-source. Your support helps fund server costs, new features, security updates, and keeps the project alive. Every contribution makes a difference!') }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ url_for('main.donate') }}" class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition-colors">
|
||||
<i class="fas fa-info-circle mr-2"></i>{{ _('Learn More') }}
|
||||
</a>
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=about_page&utm_campaign=support" target="_blank" rel="noopener" onclick="trackDonationClick('about_page')" class="px-4 py-2 bg-white hover:bg-amber-50 text-amber-600 rounded-lg font-medium transition-colors border border-amber-600">
|
||||
<i class="fas fa-mug-saucer mr-2"></i>{{ _('Donate Now') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Getting Help -->
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BuyMeACoffee Widget -->
|
||||
<!-- Support TimeTracker Widget -->
|
||||
<div class="bg-gradient-to-br from-amber-500 via-orange-500 to-amber-600 p-6 rounded-lg shadow-lg dashboard-widget animated-card text-white">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1">
|
||||
@@ -273,18 +273,36 @@
|
||||
{{ _('Support TimeTracker') }}
|
||||
</h2>
|
||||
<p class="text-sm opacity-90 mb-4">
|
||||
{{ _('Enjoying TimeTracker? Consider buying me a coffee to support continued development!') }}
|
||||
{{ _('Your support helps fund server costs, new features, and keeps TimeTracker free for everyone.') }}
|
||||
</p>
|
||||
{% if time_entries_count > 0 or total_hours > 0 %}
|
||||
<div class="text-xs opacity-75 mb-3 space-y-1">
|
||||
{% if time_entries_count > 0 %}
|
||||
<div><i class="fas fa-check-circle mr-1"></i>{{ _('You\'ve tracked %(count)s time entries', count=time_entries_count) }}</div>
|
||||
{% endif %}
|
||||
{% if total_hours > 0 %}
|
||||
<div><i class="fas fa-check-circle mr-1"></i>{{ _('You\'ve logged %(hours)s hours', hours="%.1f"|format(total_hours)) }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://buymeacoffee.com/DryTrix"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center w-full bg-white text-amber-600 px-4 py-3 rounded-lg font-semibold hover:bg-amber-50 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-mug-saucer mr-2"></i>
|
||||
{{ _('Buy me a coffee') }}
|
||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
||||
</a>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ url_for('main.donate') }}"
|
||||
class="inline-flex items-center justify-center flex-1 bg-white text-amber-600 px-4 py-3 rounded-lg font-semibold hover:bg-amber-50 transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-heart mr-2"></i>
|
||||
{{ _('Learn More') }}
|
||||
</a>
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=dashboard&utm_campaign=support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onclick="trackDonationClick('dashboard_widget')"
|
||||
class="inline-flex items-center justify-center flex-1 bg-amber-700 hover:bg-amber-800 text-white px-4 py-3 rounded-lg font-semibold transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-mug-saucer mr-2"></i>
|
||||
{{ _('Donate Now') }}
|
||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Entry Confirmation Dialogs -->
|
||||
|
||||
239
app/templates/main/donate.html
Normal file
239
app/templates/main/donate.html
Normal file
@@ -0,0 +1,239 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Support TimeTracker') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-4 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
<a href="{{ url_for('main.dashboard') }}" class="hover:text-text-light dark:hover:text-text-dark">{{ _('Dashboard') }}</a>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-text-light dark:text-text-dark">{{ _('Support Development') }}</span>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-br from-amber-500 via-orange-500 to-amber-600 p-8 rounded-lg shadow-lg text-white mb-6">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-mug-saucer text-6xl mb-4"></i>
|
||||
<h1 class="text-4xl font-bold mb-4">{{ _('Support TimeTracker Development') }}</h1>
|
||||
<p class="text-xl opacity-90 mb-6">
|
||||
{{ _('Your support helps keep TimeTracker free and continuously improving') }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=donate_page_hero&utm_campaign=support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onclick="trackDonationClick('donate_page_hero')"
|
||||
class="px-6 py-3 bg-white text-amber-600 rounded-lg font-semibold hover:bg-amber-50 transition-all shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<i class="fas fa-mug-saucer mr-2"></i>{{ _('Donate Now') }}
|
||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why Donations Matter -->
|
||||
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow border border-border-light dark:border-border-dark mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
<i class="fas fa-heart text-rose-500 mr-2"></i>
|
||||
{{ _('Why Your Support Matters') }}
|
||||
</h2>
|
||||
<div class="space-y-4 text-text-light dark:text-text-dark">
|
||||
<p class="text-lg">
|
||||
{{ _('TimeTracker is a free, open-source project built with passion and dedication. Your donations directly support:') }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-server text-primary mt-1"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-1">{{ _('Server Infrastructure') }}</h3>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Hosting, databases, and CDN costs to keep TimeTracker fast and reliable') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-code text-primary mt-1"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-1">{{ _('Feature Development') }}</h3>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('New features, improvements, and bug fixes based on your feedback') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-shield-alt text-primary mt-1"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-1">{{ _('Security & Maintenance') }}</h3>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Regular security updates, dependency maintenance, and performance optimization') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-globe text-primary mt-1"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-1">{{ _('Internationalization') }}</h3>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Translation support, localization, and making TimeTracker accessible worldwide') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your Usage Stats -->
|
||||
{% if time_entries_count > 0 or total_hours > 0 %}
|
||||
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow border border-border-light dark:border-border-dark mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
<i class="fas fa-chart-line text-primary mr-2"></i>
|
||||
{{ _('Your TimeTracker Journey') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% if days_since_signup > 0 %}
|
||||
<div class="text-center p-4 bg-background-light dark:bg-background-dark rounded-lg">
|
||||
<div class="text-3xl font-bold text-primary">{{ days_since_signup }}</div>
|
||||
<div class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
|
||||
{{ _('Days using TimeTracker') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if time_entries_count > 0 %}
|
||||
<div class="text-center p-4 bg-background-light dark:bg-background-dark rounded-lg">
|
||||
<div class="text-3xl font-bold text-primary">{{ time_entries_count }}</div>
|
||||
<div class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
|
||||
{{ _('Time entries tracked') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if total_hours > 0 %}
|
||||
<div class="text-center p-4 bg-background-light dark:bg-background-dark rounded-lg">
|
||||
<div class="text-3xl font-bold text-primary">{{ "%.1f"|format(total_hours) }}</div>
|
||||
<div class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
|
||||
{{ _('Hours tracked') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-center mt-4 text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Thank you for being part of the TimeTracker community!') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Donation Options -->
|
||||
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow border border-border-light dark:border-border-dark mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
<i class="fas fa-hand-holding-heart text-amber-500 mr-2"></i>
|
||||
{{ _('How to Support') }}
|
||||
</h2>
|
||||
<p class="mb-6 text-text-light dark:text-text-dark">
|
||||
{{ _('Every contribution, no matter the size, makes a difference. Your support helps ensure TimeTracker remains free and continues to evolve.') }}
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="https://buymeacoffee.com/DryTrix?utm_source=timetracker&utm_medium=donate_page&utm_campaign=support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onclick="trackDonationClick('donate_page_button')"
|
||||
class="inline-flex items-center justify-center gap-3 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white px-8 py-4 rounded-lg font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200">
|
||||
<i class="fas fa-mug-saucer text-2xl"></i>
|
||||
<span>{{ _('Support on Buy Me a Coffee') }}</span>
|
||||
<i class="fas fa-external-link-alt text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="text-center mt-4 text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('You\'ll be redirected to Buy Me a Coffee where you can choose your contribution amount') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Impact -->
|
||||
<div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-green-900 dark:text-green-100">
|
||||
<i class="fas fa-seedling mr-2"></i>
|
||||
{{ _('The Impact of Your Support') }}
|
||||
</h2>
|
||||
<ul class="space-y-2 text-green-800 dark:text-green-200">
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-1"></i>
|
||||
<span>{{ _('Enables faster development cycles and quicker feature releases') }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-1"></i>
|
||||
<span>{{ _('Supports better documentation and user guides') }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-1"></i>
|
||||
<span>{{ _('Helps maintain high-quality code and security standards') }}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-1"></i>
|
||||
<span>{{ _('Keeps TimeTracker free and accessible for everyone') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Alternative Ways to Help -->
|
||||
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow border border-border-light dark:border-border-dark mt-6">
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
<i class="fas fa-users text-primary mr-2"></i>
|
||||
{{ _('Other Ways to Help') }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a href="https://github.com/drytrix/TimeTracker"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-3 p-4 bg-background-light dark:bg-background-dark rounded-lg hover:bg-background-hover-light dark:hover:bg-background-hover-dark transition-colors">
|
||||
<i class="fab fa-github text-2xl text-text-light dark:text-text-dark"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ _('Contribute on GitHub') }}</div>
|
||||
<div class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Report bugs, suggest features, or submit code') }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('main.help') }}"
|
||||
class="flex items-center gap-3 p-4 bg-background-light dark:bg-background-dark rounded-lg hover:bg-background-hover-light dark:hover:bg-background-hover-dark transition-colors">
|
||||
<i class="fas fa-book text-2xl text-text-light dark:text-text-dark"></i>
|
||||
<div>
|
||||
<div class="font-semibold">{{ _('Help Others') }}</div>
|
||||
<div class="text-sm text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Share your knowledge in the community') }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function trackDonationClick(source) {
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
// Track donation link click
|
||||
fetch('{{ url_for("main.track_donation_click") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
source: source
|
||||
})
|
||||
}).catch(() => {
|
||||
// Silently fail if tracking doesn't work
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -805,8 +805,8 @@
|
||||
<a href="https://github.com/drytrix/TimeTracker/issues" target="_blank" rel="noopener" class="px-4 py-2 rounded-lg border border-amber-600 text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20">
|
||||
<i class="fas fa-bug mr-1"></i>{{ _('Report Issue') }}
|
||||
</a>
|
||||
<a href="https://buymeacoffee.com/DryTrix" target="_blank" rel="noopener" class="px-4 py-2 rounded-lg border border-green-600 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20">
|
||||
<i class="fas fa-mug-saucer mr-1"></i>{{ _('Support Development') }}
|
||||
<a href="{{ url_for('main.donate') }}" class="px-4 py-2 rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-semibold shadow-md hover:shadow-lg transition-all">
|
||||
<i class="fas fa-heart mr-1"></i>{{ _('Support Development') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
42
migrations/versions/094_add_donation_interactions.py
Normal file
42
migrations/versions/094_add_donation_interactions.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Add donation_interactions table
|
||||
|
||||
Revision ID: 094_add_donation_interactions
|
||||
Revises: 093_remove_ui_allow_flags
|
||||
Create Date: 2025-01-27 12:00:00
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "094_add_donation_interactions"
|
||||
down_revision = "093_remove_ui_allow_flags"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""Create donation_interactions table to track user interactions with donation prompts"""
|
||||
op.create_table(
|
||||
"donation_interactions",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("interaction_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("source", sa.String(length=100), nullable=True),
|
||||
sa.Column("time_entries_count", sa.Integer(), nullable=True),
|
||||
sa.Column("days_since_signup", sa.Integer(), nullable=True),
|
||||
sa.Column("total_hours", sa.Float(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_donation_interactions_user_id", "donation_interactions", ["user_id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Drop donation_interactions table"""
|
||||
op.drop_index("idx_donation_interactions_user_id", table_name="donation_interactions")
|
||||
op.drop_table("donation_interactions")
|
||||
|
||||
Reference in New Issue
Block a user