feat: subscription plan page with usage stats and plan cards

This commit is contained in:
James 2026-02-23 03:04:14 -05:00
parent fa5362401a
commit 0540d5abee
4 changed files with 173 additions and 0 deletions

View File

@ -49,6 +49,7 @@ mux.HandleFunc("/auth/logout", h.handleLogout)
mux.HandleFunc("/contacts", h.requireAuth(h.handleContacts))
mux.HandleFunc("/audit", h.requireAuth(h.handleAuditLog))
mux.HandleFunc("/analytics", h.requireAuth(h.handleAnalytics))
mux.HandleFunc("/subscription", h.requireAuth(h.handleSubscription))
// Admin CRUD
mux.HandleFunc("/admin", h.requireAdmin(h.handleAdmin))

View File

@ -0,0 +1,26 @@
package handler
import (
"net/http"
"dealroom/templates"
)
func (h *Handler) handleSubscription(w http.ResponseWriter, r *http.Request) {
profile := getProfile(r.Context())
var roomsUsed, usersUsed int
h.db.QueryRow("SELECT COUNT(*) FROM deals WHERE organization_id = ? AND is_archived = 0", profile.OrganizationID).Scan(&roomsUsed)
h.db.QueryRow("SELECT COUNT(*) FROM profiles WHERE organization_id = ?", profile.OrganizationID).Scan(&usersUsed)
stats := &templates.SubscriptionStats{
RoomsUsed: roomsUsed,
RoomsLimit: 15,
UsersUsed: usersUsed,
UsersLimit: 25,
StorageUsed: "23GB",
StorageLimit: "100GB",
}
templates.SubscriptionPage(profile, stats).Render(r.Context(), w)
}

View File

@ -54,6 +54,7 @@ templ Layout(profile *model.Profile, activePage string) {
@sidebarLink("/contacts", "Contacts", activePage == "contacts", svgUsers())
@sidebarLink("/team", "Team", activePage == "team", svgTeam())
@sidebarLink("/audit", "Audit Log", activePage == "audit", svgShield())
@sidebarLink("/subscription", "Subscription", activePage == "subscription", svgCreditCard())
@sidebarLink("/admin", "Admin", activePage == "admin", svgCog())
}
if rbac.IsBuyer(profile.Role) {
@ -164,6 +165,10 @@ templ svgTeam() {
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
}
templ svgCreditCard() {
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"></path></svg>
}
templ svgShield() {
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
}

View File

@ -0,0 +1,141 @@
package templates
import "dealroom/internal/model"
import "fmt"
type SubscriptionStats struct {
RoomsUsed int
RoomsLimit int
UsersUsed int
UsersLimit int
StorageUsed string
StorageLimit string
}
templ SubscriptionPage(profile *model.Profile, stats *SubscriptionStats) {
@Layout(profile, "subscription") {
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">Subscription</h1>
<p class="text-sm text-gray-500 mt-1">Manage your plan and usage.</p>
</div>
<!-- Current Plan -->
<div class="bg-gray-900 rounded-lg border border-teal-500/30 p-6">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">Growth Plan</h2>
<span class="text-xs px-2 py-0.5 rounded-full bg-teal-500/10 text-teal-400 font-medium">Current</span>
</div>
<p class="text-sm text-gray-500 mt-1">$799/month</p>
</div>
</div>
<div class="grid grid-cols-3 gap-6 mt-6">
<div>
<div class="flex items-center justify-between text-sm mb-1">
<span class="text-gray-400">Deal Rooms</span>
<span class="text-gray-300">{ fmt.Sprintf("%d / %d", stats.RoomsUsed, stats.RoomsLimit) }</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-2">
<div class="bg-teal-500 h-2 rounded-full" { usageWidth(stats.RoomsUsed, stats.RoomsLimit)... }> </div>
</div>
</div>
<div>
<div class="flex items-center justify-between text-sm mb-1">
<span class="text-gray-400">Users</span>
<span class="text-gray-300">{ fmt.Sprintf("%d / %d", stats.UsersUsed, stats.UsersLimit) }</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full" { usageWidth(stats.UsersUsed, stats.UsersLimit)... }> </div>
</div>
</div>
<div>
<div class="flex items-center justify-between text-sm mb-1">
<span class="text-gray-400">Storage</span>
<span class="text-gray-300">{ stats.StorageUsed } / { stats.StorageLimit }</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-2">
<div class="bg-amber-500 h-2 rounded-full" style="width: 23%"> </div>
</div>
</div>
</div>
</div>
<!-- Plan Cards -->
<div class="grid grid-cols-3 gap-6">
<!-- Starter -->
<div class="bg-gray-900 rounded-lg border border-gray-800 p-6 flex flex-col">
<h3 class="text-lg font-semibold">Starter</h3>
<p class="text-3xl font-bold mt-2">$299<span class="text-sm font-normal text-gray-500">/mo</span></p>
<ul class="mt-4 space-y-2 text-sm text-gray-400 flex-1">
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
3 deal rooms
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
5 users
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
10GB storage
</li>
</ul>
<button onclick="alert('Please contact sales@dealspace.ai to change your plan.')" class="mt-6 w-full py-2 rounded-lg border border-gray-700 text-sm text-gray-400 hover:bg-gray-800 transition">Downgrade</button>
</div>
<!-- Growth -->
<div class="bg-gray-900 rounded-lg border-2 border-teal-500 p-6 flex flex-col relative">
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-0.5 bg-teal-500 text-white text-xs font-medium rounded-full">Current Plan</div>
<h3 class="text-lg font-semibold">Growth</h3>
<p class="text-3xl font-bold mt-2 text-teal-400">$799<span class="text-sm font-normal text-gray-500">/mo</span></p>
<ul class="mt-4 space-y-2 text-sm text-gray-300 flex-1">
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-teal-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
15 deal rooms
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-teal-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
25 users
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-teal-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
100GB storage
</li>
</ul>
<button disabled class="mt-6 w-full py-2 rounded-lg bg-teal-500/20 text-teal-400 text-sm font-medium cursor-not-allowed">Active</button>
</div>
<!-- Enterprise -->
<div class="bg-gray-900 rounded-lg border border-gray-800 p-6 flex flex-col">
<h3 class="text-lg font-semibold">Enterprise</h3>
<p class="text-3xl font-bold mt-2">Custom</p>
<ul class="mt-4 space-y-2 text-sm text-gray-400 flex-1">
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
Unlimited deal rooms
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
Unlimited users
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
1TB storage
</li>
</ul>
<button onclick="alert('Please contact sales@dealspace.ai to discuss Enterprise pricing.')" class="mt-6 w-full py-2 rounded-lg bg-purple-500 text-white text-sm font-medium hover:bg-purple-600 transition">Contact Sales</button>
</div>
</div>
</div>
}
}
func usageWidth(used int, limit int) templ.Attributes {
pct := 0
if limit > 0 {
pct = (used * 100) / limit
}
return templ.Attributes{"style": fmt.Sprintf("width: %d%%", pct)}
}