UNPKG

questro

Version:

A lightweight, modular gamification library for React with unique visual components. Features combo meters, daily challenges, achievement toasts, and progress rings. Add points, badges, quests, leaderboards, levels/XP, streaks, and notifications with zero

2 lines 10.4 kB
import {a}from'./chunk-R2GD7YTX.mjs';import I,{createContext,useMemo,useState,useEffect,useCallback}from'react';import {jsx,jsxs,Fragment}from'react/jsx-runtime';function k(a,t,r=3){if(!a)return false;let s=Date.now(),e=(s-a)/(1e3*60*60);switch(t){case "daily":{let o=new Date(a),i=new Date(s);o.setHours(0,0,0,0),i.setHours(0,0,0,0);let c=Math.floor((i.getTime()-o.getTime())/(1e3*60*60*24));return c===0?true:c===1?new Date(s).getHours()+new Date(s).getMinutes()/60<=r:false}case "weekly":return e/24<=7+r/24;case "monthly":{let o=new Date(a),i=new Date(s);if(o.getMonth()===i.getMonth()&&o.getFullYear()===i.getFullYear())return true;let c=new Date(i);return c.setMonth(c.getMonth()-1),o.getMonth()===c.getMonth()&&o.getFullYear()===c.getFullYear()?i.getDate()<=Math.ceil(r/24):false}}}function U(a,t){if(a.length===0)return false;let r=new Date,s=a[a.length-1];if(!s)return false;let n=new Date(s.timestamp);switch(t){case "daily":return n.getFullYear()===r.getFullYear()&&n.getMonth()===r.getMonth()&&n.getDate()===r.getDate();case "weekly":{let e=o=>{let i=new Date(o);i.setHours(0,0,0,0),i.setDate(i.getDate()+4-(i.getDay()||7));let c=new Date(i.getFullYear(),0,1);return Math.ceil(((i.getTime()-c.getTime())/864e5+1)/7)};return e(n)===e(r)&&n.getFullYear()===r.getFullYear()}case "monthly":return n.getFullYear()===r.getFullYear()&&n.getMonth()===r.getMonth()}}function K(a,t){if(a.length===0)return 0;let r=[...a].filter(e=>e.completed).sort((e,o)=>o.timestamp-e.timestamp);if(r.length===0)return 0;let s=0,n=new Date;for(let e=0;e<r.length;e++){let o=r[e];if(!o)continue;let i=new Date(o.timestamp);if(e===0){if(!H(i,n,t))return 0;s=1;continue}let c=r[e-1];if(!c)break;let u=new Date(c.timestamp);if(P(i,u,t))s++;else break}return s}function H(a,t,r){switch(r){case "daily":return a.getFullYear()===t.getFullYear()&&a.getMonth()===t.getMonth()&&(a.getDate()===t.getDate()||a.getDate()===t.getDate()-1);case "weekly":return Math.floor((t.getTime()-a.getTime())/6048e5)<=1;case "monthly":return a.getFullYear()===t.getFullYear()&&a.getMonth()===t.getMonth()||a.getFullYear()===t.getFullYear()-1&&a.getMonth()===11&&t.getMonth()===0||a.getMonth()===t.getMonth()-1}}function P(a,t,r){switch(r){case "daily":return Math.floor((t.getTime()-a.getTime())/864e5)===1;case "weekly":return Math.floor((t.getTime()-a.getTime())/6048e5)===1;case "monthly":return (t.getFullYear()-a.getFullYear())*12+(t.getMonth()-a.getMonth())===1}}function y(a){let t=a.getFullYear(),r=String(a.getMonth()+1).padStart(2,"0"),s=String(a.getDate()).padStart(2,"0");return `${t}-${r}-${s}`}function M(a){let t=new Date;switch(a){case "daily":return y(t);case "weekly":{let r=new Date(t),s=r.getDay(),n=r.getDate()-s+(s===0?-6:1);return r.setDate(n),y(r)}case "monthly":return `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}`}}var m=class{constructor(t,r){this.listeners=new Set;this.config={userId:t.userId,type:t.type||"daily",graceHours:t.graceHours??3,maxFreezes:t.maxFreezes??3,milestones:t.milestones,onStreakUpdate:t.onStreakUpdate,onMilestoneReached:t.onMilestoneReached,onStreakBroken:t.onStreakBroken},this.state=r||this.createInitialState(),this.checkStreakStatus();}createInitialState(){return {userId:this.config.userId,type:this.config.type,current:0,longest:0,lastActivity:null,startDate:null,freezes:this.config.maxFreezes,history:[],milestonesReached:[],lastUpdated:Date.now()}}getStreakData(){let t=this.state.lastActivity?k(this.state.lastActivity,this.config.type,this.config.graceHours):false;return {type:this.config.type,current:this.state.current,longest:this.state.longest,lastActivity:this.state.lastActivity,startDate:this.state.startDate,freezes:this.state.freezes,isActive:t,timeUntilBreak:this.calculateTimeUntilBreak()}}recordActivity(){let t=Date.now();if(U(this.state.history,this.config.type))return;let r=this.state.lastActivity?k(this.state.lastActivity,this.config.type,this.config.graceHours):false,s=this.state.current;!r&&s>0&&this.handleStreakBroken(s);let n={date:M(this.config.type),completed:true,timestamp:t};if(this.state.history.push(n),this.state.lastActivity=t,r?this.state.current++:(this.state.current=1,this.state.startDate=t),this.state.current>this.state.longest&&(this.state.longest=this.state.current),this.state.lastUpdated=t,this.checkMilestones(),this.config.onStreakUpdate){let e={previousStreak:s,newStreak:this.state.current,timestamp:t,broken:!r&&s>0};this.config.onStreakUpdate(e);}this.notifyListeners();}useFreeze(){if(this.state.freezes<=0||(this.state.lastActivity?k(this.state.lastActivity,this.config.type,this.config.graceHours):false))return false;this.state.freezes--;let r={date:M(this.config.type),completed:false,timestamp:Date.now(),freezeUsed:true};return this.state.history.push(r),this.state.lastActivity=Date.now(),this.state.lastUpdated=Date.now(),this.notifyListeners(),true}reset(){let t=this.state.current;this.state=this.createInitialState(),t>0&&this.config.onStreakBroken&&this.config.onStreakBroken(t),this.notifyListeners();}getHistory(){return [...this.state.history]}getCalendarData(t,r){let s=new Date(r,t+1,0).getDate(),n=new Date,e=[];for(let o=1;o<=s;o++){let i=new Date(r,t,o),c=y(i),u=this.state.history.find(d=>d.date===c),h=i.getDate()===n.getDate()&&i.getMonth()===n.getMonth()&&i.getFullYear()===n.getFullYear(),l=i.getTime()>n.getTime();e.push({date:c,completed:u?.completed??false,isToday:h,isFuture:l,freezeUsed:u?.freezeUsed});}return e}getState(){return {...this.state}}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}checkStreakStatus(){if(!this.state.lastActivity||this.state.current===0)return;k(this.state.lastActivity,this.config.type,this.config.graceHours)||this.handleStreakBroken(this.state.current);}handleStreakBroken(t){this.state.current=0,this.state.startDate=null,this.config.onStreakBroken&&this.config.onStreakBroken(t);}checkMilestones(){if(this.config.milestones)for(let t of this.config.milestones)this.state.current===t.streak&&!this.state.milestonesReached.includes(t.streak)&&(this.state.milestonesReached.push(t.streak),t.reward.freeze&&(this.state.freezes=Math.min(this.state.freezes+t.reward.freeze,this.config.maxFreezes)),this.config.onMilestoneReached&&this.config.onMilestoneReached(t,this.state.current));}calculateTimeUntilBreak(){if(!this.state.lastActivity||this.state.current===0)return 0;let t=Date.now(),r=(t-this.state.lastActivity)/(1e3*60*60);switch(this.config.type){case "daily":return Math.max(0,24+this.config.graceHours-r);case "weekly":return Math.max(0,168+this.config.graceHours-r);case "monthly":{let s=new Date(this.state.lastActivity),n=new Date(s);return n.setMonth(n.getMonth()+1),n.setDate(Math.ceil(this.config.graceHours/24)),Math.max(0,(n.getTime()-t)/(1e3*60*60))}}}notifyListeners(){this.listeners.forEach(t=>t());}};var B=createContext(null);function X({children:a$1,config:t,storage:r}){let s=useMemo(()=>r||new a,[r]),n=`questro_streaks_${t.userId}_${t.type||"daily"}`,[e,o]=useState(null),[i,c]=useState(null),[u,h]=useState([]);useEffect(()=>{let p=true;return (async()=>{let E=await s.get(n);if(!p)return;let w=new m(t,E||void 0);o(w),c(w.getStreakData()),h(w.getHistory());})(),()=>{p=false;}},[t,s,n]),useEffect(()=>e?e.subscribe(()=>{c(e.getStreakData()),h(e.getHistory()),s.set(n,e.getState());}):void 0,[e,s,n]);let l=useCallback(()=>{e?.recordActivity();},[e]),d=useCallback(()=>e?.useFreeze()??false,[e]),x=useCallback(async()=>{e&&(e.reset(),await s.remove(n));},[e,s,n]),A=useCallback((p,z)=>e?.getCalendarData(p,z)??[],[e]),C=useMemo(()=>!e||!i?null:{streakData:i,history:u,recordActivity:l,useFreeze:d,reset:x,getCalendarData:A},[e,i,u,l,d,x,A]);return C?jsx(B.Provider,{value:C,children:a$1}):null}function D(){let a=I.useContext(B);if(!a)throw new Error("useStreaks must be used within a StreaksProvider");return a}function et({showFreezes:a=true,showLongest:t=false,showWarning:r=true,className:s,children:n}){let{streakData:e}=D();return n?jsx(Fragment,{children:n({current:e.current,longest:e.longest,freezes:e.freezes,isActive:e.isActive,timeUntilBreak:e.timeUntilBreak})}):jsxs("div",{className:s,"data-component":"streak-display",children:[jsxs("div",{"data-streak-main":true,children:[jsx("span",{"data-streak-icon":true,children:"\u{1F525}"}),jsx("span",{"data-streak-number":true,children:e.current}),jsxs("span",{"data-streak-label":true,children:[e.type," streak"]})]}),t&&e.longest>0&&jsxs("div",{"data-streak-longest":true,children:["Longest: ",e.longest]}),a&&e.freezes>0&&jsxs("div",{"data-streak-freezes":true,children:["\u2744\uFE0F ",e.freezes," freeze",e.freezes!==1?"s":""]}),r&&!e.isActive&&e.current>0&&jsxs("div",{"data-streak-warning":true,children:["\u26A0\uFE0F Complete"," ",e.type==="daily"?"today":"this "+e.type.replace("ly","")," to keep streak!"]})]})}function it({month:a,year:t,className:r,renderDay:s,children:n}){let{getCalendarData:e}=D(),o=new Date,i=a??o.getMonth(),c=t??o.getFullYear(),u=e(i,c);if(n)return jsx(Fragment,{children:n({days:u,month:i,year:c})});let h=new Date(c,i).toLocaleString("default",{month:"long"});return jsxs("div",{className:r,"data-component":"streak-calendar",children:[jsx("div",{"data-calendar-header":true,children:jsxs("h3",{children:[h," ",c]})}),jsxs("div",{"data-calendar-grid":true,style:{display:"grid",gridTemplateColumns:"repeat(7, 1fr)",gap:"4px"},children:[["S","M","T","W","T","F","S"].map((l,d)=>jsx("div",{"data-calendar-weekday":true,style:{textAlign:"center",fontWeight:"bold",padding:"4px"},children:l},d)),u.map(l=>s?jsx(I.Fragment,{children:s(l)},l.date):jsxs("div",{"data-calendar-day":true,"data-completed":l.completed,"data-is-today":l.isToday,"data-is-future":l.isFuture,"data-freeze-used":l.freezeUsed,style:{padding:"8px",textAlign:"center",borderRadius:"4px",backgroundColor:l.completed?"#4ade80":l.freezeUsed?"#60a5fa":l.isFuture?"#f3f4f6":"#fca5a5",color:l.completed||l.freezeUsed?"white":"black",opacity:l.isFuture?.5:1},children:[new Date(l.date).getDate(),l.completed&&" \u2713",l.freezeUsed&&" \u2744\uFE0F"]},l.date))]}),jsx("div",{"data-calendar-legend":true,style:{marginTop:"12px",fontSize:"12px"},children:jsx("div",{children:"\u{1F7E2} Completed | \u{1F535} Freeze Used | \u{1F534} Missed | \u26AA Future"})})]})}export{k as a,U as b,K as c,y as d,M as e,m as f,X as g,D as h,et as i,it as j};//# sourceMappingURL=chunk-I66CL7SM.mjs.map //# sourceMappingURL=chunk-I66CL7SM.mjs.map