208 lines
6.5 KiB
Go
208 lines
6.5 KiB
Go
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,
|
|
}
|
|
}
|