feat: add social features, leaderboard, auth enhancements, and admin fixes
continuous-integration/drone/push Build is passing

- Implement Social Features: Shared Tasks, Global Access, Privacy Settings (Leaderboard/Searchable).
- Add Leaderboard Page and API.
- Enhance Auth: Support Email/Username login, explicit duplicate registration errors.
- Fix: Admin login password hash regression.
- Refactor: Move to wouter for routing, add Admin Dashboard and User Management.
- Add Setup Wizard.
- Update UI with Sidebar and Gamification elements.
This commit is contained in:
2025-12-10 14:04:26 +01:00
parent d5b045158a
commit ccfb674318
52 changed files with 9206 additions and 1505 deletions
+82
View File
@@ -0,0 +1,82 @@
interface SubtaskTemplate {
keywords: string[];
subtasks: string[];
}
const TEMPLATES: SubtaskTemplate[] = [
{
keywords: ['vacation', 'trip', 'travel', 'holiday', 'flight'],
subtasks: [
'- [ ] Research destination and dates',
'- [ ] Book flights',
'- [ ] Reserve accommodation',
'- [ ] Check passport validity',
'- [ ] Create packing list',
'- [ ] Arrange transport to airport'
]
},
{
keywords: ['feature', 'coding', 'code', 'bug', 'fix', 'implement', 'dev'],
subtasks: [
'- [ ] Analyze requirements',
'- [ ] Design technical approach',
'- [ ] Write implementation code',
'- [ ] Write unit tests',
'- [ ] Perform manual verification',
'- [ ] Open Pull Request'
]
},
{
keywords: ['groceries', 'shop', 'buy', 'store', 'market'],
subtasks: [
'- [ ] Check fridge/pantry inventory',
'- [ ] Plan meals for the week',
'- [ ] Write shopping list',
'- [ ] Bring reusable bags',
'- [ ] Go to store'
]
},
{
keywords: ['meeting', 'call', 'sync', 'discussion', 'interview'],
subtasks: [
'- [ ] Prepare agenda',
'- [ ] Review background materials',
'- [ ] Set up video conference link',
'- [ ] Take notes during meeting',
'- [ ] Send follow-up action items'
]
},
{
keywords: ['clean', 'tidy', 'organize', 'house', 'chore'],
subtasks: [
'- [ ] Gather cleaning supplies',
'- [ ] Declutter surface areas',
'- [ ] Dust and wipe down',
'- [ ] Vacuum/Sweep floors',
'- [ ] Take out trash'
]
}
];
const GENERIC_SUBTASKS = [
'- [ ] Define success criteria',
'- [ ] Break down into smaller steps',
'- [ ] Execute first step',
'- [ ] Review progress',
'- [ ] Mark as complete'
];
export async function simulateAIDecomposition(taskTitle: string): Promise<string> {
// Simulate network latency for "AI" feel
await new Promise(resolve => setTimeout(resolve, 1500));
const lowerTitle = taskTitle.toLowerCase();
const match = TEMPLATES.find(t =>
t.keywords.some(k => lowerTitle.includes(k))
);
const steps = match ? match.subtasks : GENERIC_SUBTASKS;
return `\n\n### 🪄 AI Suggested Steps:\n${steps.join('\n')}`;
}
+86
View File
@@ -0,0 +1,86 @@
export class AmbientSoundPlayer {
private audioCtx: AudioContext | null = null;
private noiseSource: AudioBufferSourceNode | null = null;
private gainNode: GainNode | null = null;
private isPlaying = false;
constructor() {
// Initialize audio context only on user interaction to comply with browser policies
}
private initContext() {
if (!this.audioCtx) {
this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
}
}
private createBrownNoiseBuffer(): AudioBuffer {
if (!this.audioCtx) throw new Error("No Audio Context");
const bufferSize = this.audioCtx.sampleRate * 2; // 2 seconds buffer
const buffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate);
const data = buffer.getChannelData(0);
let lastOut = 0;
for (let i = 0; i < bufferSize; i++) {
const white = Math.random() * 2 - 1;
data[i] = (lastOut + (0.02 * white)) / 1.02;
lastOut = data[i];
data[i] *= 3.5; // Compensate for gain loss
}
return buffer;
}
public toggle(type: 'brown' | 'white' = 'brown'): boolean {
if (this.isPlaying) {
this.stop();
return false;
} else {
this.play(type);
return true;
}
}
public play(type: 'brown' | 'white' = 'brown') {
this.initContext();
if (!this.audioCtx) return;
// Resume if suspended (browser autoplay policy)
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
this.stop(); // Stop any current sound
this.noiseSource = this.audioCtx.createBufferSource();
this.noiseSource.buffer = this.createBrownNoiseBuffer(); // Currently only brown noise implemented efficiently
this.noiseSource.loop = true;
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = 0.5; // Default volume
this.noiseSource.connect(this.gainNode);
this.gainNode.connect(this.audioCtx.destination);
// Fade in
this.gainNode.gain.setValueAtTime(0, this.audioCtx.currentTime);
this.gainNode.gain.linearRampToValueAtTime(0.5, this.audioCtx.currentTime + 1);
this.noiseSource.start();
this.isPlaying = true;
}
public stop() {
if (this.noiseSource && this.gainNode && this.audioCtx) {
// Fade out
this.gainNode.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + 0.5);
setTimeout(() => {
this.noiseSource?.stop();
this.noiseSource = null;
}, 500);
}
this.isPlaying = false;
}
}
export const ambientPlayer = new AmbientSoundPlayer();
+37
View File
@@ -0,0 +1,37 @@
import confetti from 'canvas-confetti';
export const triggerConfetti = (x: number, y: number) => {
const defaults = { origin: { x, y } };
const count = 200;
function fire(particleRatio: number, opts: confetti.Options) {
confetti({
...defaults,
...opts,
particleCount: Math.floor(count * particleRatio),
});
}
fire(0.25, {
spread: 26,
startVelocity: 55,
});
fire(0.2, {
spread: 60,
});
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
});
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
});
fire(0.1, {
spread: 120,
startVelocity: 45,
});
};
+47
View File
@@ -0,0 +1,47 @@
export const LEVEL_THRESHOLDS = [
0, // Level 1: 0-99
100, // Level 2: 100-249
250, // Level 3: 250-499
500, // Level 4: 500-999
1000, // Level 5: 1000-1999
2000, // Level 6: 2000-3499
3500, // Level 7: 3500-4999
5000, // Level 8: 5000-7499
7500, // Level 9: 7500-9999
10000 // Level 10: 10000+
];
export function getLevelFromXP(xp: number): number {
for (let i = LEVEL_THRESHOLDS.length - 1; i >= 0; i--) {
if (xp >= LEVEL_THRESHOLDS[i]) {
return i + 1;
}
}
return 1;
}
export function getNextLevelXP(level: number): number {
if (level >= LEVEL_THRESHOLDS.length) {
return LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 1] * 1.5; // Scale indefinitely
}
return LEVEL_THRESHOLDS[level];
}
export function getLevelProgress(xp: number): number {
const currentLevel = getLevelFromXP(xp);
const currentLevelStart = LEVEL_THRESHOLDS[currentLevel - 1];
const nextLevelStart = getNextLevelXP(currentLevel);
if (xp >= nextLevelStart) return 100;
const progress = ((xp - currentLevelStart) / (nextLevelStart - currentLevelStart)) * 100;
return Math.min(100, Math.max(0, progress));
}
export function getRankKey(level: number): string {
if (level >= 50) return 'master';
if (level >= 20) return 'architect';
if (level >= 10) return 'planner';
if (level >= 5) return 'builder';
return 'novice';
}
+53
View File
@@ -0,0 +1,53 @@
export interface ParsedTask {
title: string;
priority?: 'low' | 'medium' | 'high';
dueDate?: Date;
labelName?: string;
}
export const parseTaskInput = (input: string): ParsedTask => {
let title = input;
let priority: ParsedTask['priority'] | undefined;
let dueDate: Date | undefined;
let labelName: string | undefined;
// Parse Priority (!high, !medium, !low)
const priorityMatch = title.match(/!(high|medium|low)/i);
if (priorityMatch) {
priority = priorityMatch[1].toLowerCase() as ParsedTask['priority'];
title = title.replace(priorityMatch[0], '').trim();
}
// Parse Label (#work, #personal)
const labelMatch = title.match(/#(\w+)/);
if (labelMatch) {
labelName = labelMatch[1];
title = title.replace(labelMatch[0], '').trim();
}
// Parse Date (tomorrow, today, next friday) - Simple heuristic
// Note: For production capability, use 'chrono-node'
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (title.match(/\btomorrow\b/i)) {
dueDate = tomorrow;
title = title.replace(/\btomorrow\b/i, '').trim();
} else if (title.match(/\btoday\b/i)) {
dueDate = today;
title = title.replace(/\btoday\b/i, '').trim();
} else if (title.match(/\bnext week\b/i)) {
const nextWeek = new Date(today);
nextWeek.setDate(today.getDate() + 7);
dueDate = nextWeek;
title = title.replace(/\bnext week\b/i, '').trim();
}
return {
title,
priority,
dueDate,
labelName
};
};
+31
View File
@@ -0,0 +1,31 @@
// Simple sound effects using base64 or public URLs to avoid asset management issues for this demo
// Using a short "pop" sound
const POP_SOUND = "data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU"; // Placeholder, real sound below
export const playSuccessSound = () => {
// A simple pleasant "pop" sound frequency sequence using Web Audio API for zero-dependency
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(800, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.1);
} catch (e) {
console.error("Audio play failed", e);
}
};