writekit/internal/server/demo.go

209 lines
6.5 KiB
Go
Raw Normal View History

2026-01-09 00:16:46 +02:00
package server
import (
"bytes"
"math/rand"
"net/http"
"os"
"strconv"
"time"
"github.com/writekitapp/writekit/internal/auth"
"github.com/writekitapp/writekit/internal/db"
"github.com/writekitapp/writekit/internal/tenant"
)
type ctxKey string
const (
tenantIDKey ctxKey = "tenantID"
demoInfoKey ctxKey = "demoInfo"
)
type DemoInfo struct {
IsDemo bool
ExpiresAt time.Time
}
func GetDemoInfo(r *http.Request) DemoInfo {
if info, ok := r.Context().Value(demoInfoKey).(DemoInfo); ok {
return info
}
return DemoInfo{}
}
func demoAwareSessionMiddleware(database *db.DB) func(http.Handler) http.Handler {
sessionMW := auth.SessionMiddleware(database)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if GetDemoInfo(r).IsDemo {
next.ServeHTTP(w, r)
return
}
sessionMW(next).ServeHTTP(w, r)
})
}
}
func (s *Server) injectDemoBanner(html []byte, expiresAt time.Time) []byte {
scheme := "https"
if os.Getenv("ENV") != "prod" {
scheme = "http"
}
redirectURL := scheme + "://" + s.domain
banner := demoBannerHTML(expiresAt, redirectURL)
return bytes.Replace(html, []byte("</body>"), append([]byte(banner), []byte("</body>")...), 1)
}
func demoBannerHTML(expiresAt time.Time, redirectURL string) string {
expiresUnix := expiresAt.Unix()
return `<div id="demo-banner"><div class="demo-inner"><span class="demo-timer"></span><a class="demo-cta" target="_blank" href="/studio">Open Studio</a></div></div>
<style>
#demo-banner{position:fixed;bottom:0;left:0;right:0;background:rgba(220,38,38,0.95);color:#fff;font-family:system-ui,-apple-system,sans-serif;font-size:13px;z-index:99999;backdrop-filter:blur(8px);padding:10px 16px}
@media(min-width:640px){#demo-banner{bottom:auto;top:0;left:auto;right:16px;width:auto;padding:8px 16px}}
.demo-inner{display:flex;align-items:center;justify-content:center;gap:12px}
.demo-timer{font-variant-numeric:tabular-nums;font-weight:500}
.demo-timer.urgent{color:#fecaca;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
.demo-cta{background:#fff;color:#dc2626;padding:6px 12px;text-decoration:none;font-weight:600;font-size:12px;transition:transform .15s}
.demo-cta:hover{transform:scale(1.05)}
</style>
<script>
(function(){
const exp=` + strconv.FormatInt(expiresUnix, 10) + `*1000;
const timer=document.querySelector('.demo-timer');
const cta=document.querySelector('.demo-cta');
const update=()=>{
const left=Math.max(0,exp-Date.now());
const m=Math.floor(left/60000);
const s=Math.floor((left%60000)/1000);
timer.textContent=m+':'+(s<10?'0':'')+s+' remaining';
if(left<30000)timer.classList.add('urgent');
if(left<=0){
timer.textContent='Demo expired';
setTimeout(()=>{
const sub=location.hostname.split('.')[0];
location.href='` + redirectURL + `?expired=true&subdomain='+sub;
},2000);
return;
}
requestAnimationFrame(update);
};
update();
if(location.pathname.startsWith('/studio'))cta.textContent='View Site',cta.href='/posts';
})();
</script>`
}
func generateFakeAnalytics(days int) *tenant.AnalyticsSummary {
if days <= 0 {
days = 30
}
baseViews := 25 + rand.Intn(20)
var totalViews, totalVisitors int64
viewsByDay := make([]tenant.DailyStats, days)
for i := 0; i < days; i++ {
date := time.Now().AddDate(0, 0, -days+i+1)
weekday := date.Weekday()
multiplier := 1.0
if weekday == time.Saturday || weekday == time.Sunday {
multiplier = 0.6
} else if weekday == time.Monday {
multiplier = 1.2
}
dailyViews := int64(float64(baseViews+rand.Intn(15)) * multiplier)
dailyVisitors := dailyViews * int64(65+rand.Intn(15)) / 100
totalViews += dailyViews
totalVisitors += dailyVisitors
viewsByDay[i] = tenant.DailyStats{
Date: date.Format("2006-01-02"),
Views: dailyViews,
Visitors: dailyVisitors,
}
}
return &tenant.AnalyticsSummary{
TotalViews: totalViews,
TotalPageViews: totalViews,
UniqueVisitors: totalVisitors,
TotalBandwidth: totalViews * 45000,
ViewsChange: float64(rand.Intn(300)-100) / 10,
ViewsByDay: viewsByDay,
TopPages: []tenant.PageStats{
{Path: "/", Views: totalViews * 25 / 100},
{Path: "/posts/shipping-a-side-project", Views: totalViews * 22 / 100},
{Path: "/posts/debugging-production-like-a-detective", Views: totalViews * 18 / 100},
{Path: "/posts/sqlite-in-production", Views: totalViews * 15 / 100},
{Path: "/posts", Views: totalViews * 12 / 100},
{Path: "/posts/my-2024-reading-list", Views: totalViews * 8 / 100},
},
TopReferrers: []tenant.ReferrerStats{
{Referrer: "Google", Views: totalViews * 30 / 100},
{Referrer: "Twitter/X", Views: totalViews * 20 / 100},
{Referrer: "GitHub", Views: totalViews * 15 / 100},
{Referrer: "Hacker News", Views: totalViews * 12 / 100},
{Referrer: "LinkedIn", Views: totalViews * 10 / 100},
{Referrer: "YouTube", Views: totalViews * 8 / 100},
{Referrer: "Reddit", Views: totalViews * 5 / 100},
},
Browsers: []tenant.NamedStat{
{Name: "Chrome", Count: totalVisitors * 55 / 100},
{Name: "Safari", Count: totalVisitors * 25 / 100},
{Name: "Firefox", Count: totalVisitors * 12 / 100},
{Name: "Edge", Count: totalVisitors * 8 / 100},
},
OS: []tenant.NamedStat{
{Name: "macOS", Count: totalVisitors * 45 / 100},
{Name: "Windows", Count: totalVisitors * 30 / 100},
{Name: "iOS", Count: totalVisitors * 15 / 100},
{Name: "Linux", Count: totalVisitors * 7 / 100},
{Name: "Android", Count: totalVisitors * 3 / 100},
},
Devices: []tenant.NamedStat{
{Name: "Desktop", Count: totalVisitors * 70 / 100},
{Name: "Mobile", Count: totalVisitors * 25 / 100},
{Name: "Tablet", Count: totalVisitors * 5 / 100},
},
Countries: []tenant.NamedStat{
{Name: "United States", Count: totalVisitors * 40 / 100},
{Name: "United Kingdom", Count: totalVisitors * 12 / 100},
{Name: "Germany", Count: totalVisitors * 10 / 100},
{Name: "Canada", Count: totalVisitors * 8 / 100},
{Name: "France", Count: totalVisitors * 6 / 100},
{Name: "Australia", Count: totalVisitors * 5 / 100},
},
}
}
func generateFakePostAnalytics(days int) *tenant.AnalyticsSummary {
if days <= 0 {
days = 30
}
baseViews := 5 + rand.Intn(8)
var totalViews int64
viewsByDay := make([]tenant.DailyStats, days)
for i := 0; i < days; i++ {
date := time.Now().AddDate(0, 0, -days+i+1)
dailyViews := int64(baseViews + rand.Intn(6))
totalViews += dailyViews
viewsByDay[i] = tenant.DailyStats{
Date: date.Format("2006-01-02"),
Views: dailyViews,
}
}
return &tenant.AnalyticsSummary{
TotalViews: totalViews,
ViewsByDay: viewsByDay,
}
}