tasks: shared task board with sticker rewards, difficulty levels, and calendar
CI / update (push) Has been cancelled
CI / update (push) Has been cancelled
Complete household task management system behind task_users auth group: - Task CRUD with recurring schedules, assignees, tags, and optional difficulty - Blobcat SVG sticker rewards on completion, rarity weighted by difficulty - Sticker collection page with calendar view and progress tracking - Redesigned cards with left accent urgency strip, assignee PFP, round check button - Weekday-based due date labels for tasks within 7 days - Tasks link added to homepage LinksGrid
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Task } from '$models/Task';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const tasks = await Task.find({ active: true })
|
||||
.sort({ nextDueDate: 1 })
|
||||
.lean();
|
||||
|
||||
return json({ tasks });
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
const data = await request.json();
|
||||
const { title, description, assignees, tags, difficulty, isRecurring, frequency, nextDueDate } = data;
|
||||
|
||||
if (!title?.trim()) throw error(400, 'Title is required');
|
||||
if (!nextDueDate) throw error(400, 'Due date is required');
|
||||
if (isRecurring && !frequency?.type) throw error(400, 'Frequency is required for recurring tasks');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const task = await Task.create({
|
||||
title: title.trim(),
|
||||
description: description?.trim(),
|
||||
assignees: assignees || [],
|
||||
tags: tags || [],
|
||||
difficulty: difficulty || undefined,
|
||||
isRecurring: !!isRecurring,
|
||||
frequency: isRecurring ? frequency : undefined,
|
||||
nextDueDate: new Date(nextDueDate),
|
||||
createdBy: auth.user.nickname,
|
||||
active: true
|
||||
});
|
||||
|
||||
return json({ task }, { status: 201 });
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Task } from '$models/Task';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
const data = await request.json();
|
||||
const { title, description, assignees, tags, difficulty, isRecurring, frequency, nextDueDate, active } = data;
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const task = await Task.findById(params.id);
|
||||
if (!task) throw error(404, 'Task not found');
|
||||
|
||||
if (title !== undefined) task.title = title.trim();
|
||||
if (description !== undefined) task.description = description?.trim();
|
||||
if (assignees !== undefined) task.assignees = assignees;
|
||||
if (tags !== undefined) task.tags = tags;
|
||||
if (difficulty !== undefined) task.difficulty = difficulty || undefined;
|
||||
if (isRecurring !== undefined) {
|
||||
task.isRecurring = isRecurring;
|
||||
task.frequency = isRecurring ? frequency : undefined;
|
||||
}
|
||||
if (nextDueDate !== undefined) task.nextDueDate = new Date(nextDueDate);
|
||||
if (active !== undefined) task.active = active;
|
||||
|
||||
await task.save();
|
||||
return json({ task });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const task = await Task.findById(params.id);
|
||||
if (!task) throw error(404, 'Task not found');
|
||||
|
||||
// Soft delete
|
||||
task.active = false;
|
||||
await task.save();
|
||||
|
||||
return json({ success: true });
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Task } from '$models/Task';
|
||||
import { TaskCompletion } from '$models/TaskCompletion';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { getStickerForTags } from '$lib/utils/stickers';
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
function getNextDueDate(completedAt: Date, frequencyType: string, customDays?: number): Date {
|
||||
switch (frequencyType) {
|
||||
case 'daily': return addDays(completedAt, 1);
|
||||
case 'weekly': return addDays(completedAt, 7);
|
||||
case 'biweekly': return addDays(completedAt, 14);
|
||||
case 'monthly': {
|
||||
const next = new Date(completedAt);
|
||||
next.setMonth(next.getMonth() + 1);
|
||||
return next;
|
||||
}
|
||||
case 'custom': return addDays(completedAt, customDays || 7);
|
||||
default: return addDays(completedAt, 7);
|
||||
}
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ params, locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const task = await Task.findById(params.id);
|
||||
if (!task) throw error(404, 'Task not found');
|
||||
if (!task.active) throw error(400, 'Task is archived');
|
||||
|
||||
const now = new Date();
|
||||
const nickname = auth.user.nickname;
|
||||
|
||||
// Award a sticker based on task tags and difficulty
|
||||
const sticker = getStickerForTags(task.tags, task.difficulty || 'medium');
|
||||
|
||||
// Record the completion
|
||||
const completion = await TaskCompletion.create({
|
||||
taskId: task._id,
|
||||
taskTitle: task.title,
|
||||
completedBy: nickname,
|
||||
completedAt: now,
|
||||
stickerId: sticker.id,
|
||||
tags: task.tags
|
||||
});
|
||||
|
||||
// Update task
|
||||
task.lastCompletedAt = now;
|
||||
task.lastCompletedBy = nickname;
|
||||
|
||||
if (task.isRecurring && task.frequency) {
|
||||
// Reset from NOW (completion time), not from the original due date
|
||||
task.nextDueDate = getNextDueDate(now, task.frequency.type, task.frequency.customDays);
|
||||
} else {
|
||||
// One-off task: deactivate
|
||||
task.active = false;
|
||||
}
|
||||
|
||||
await task.save();
|
||||
|
||||
return json({ completion, sticker, task });
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { TaskCompletion } from '$models/TaskCompletion';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth?.user?.nickname) throw error(401, 'Not logged in');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
// Completions per user
|
||||
const userStats = await TaskCompletion.aggregate([
|
||||
{ $group: { _id: '$completedBy', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]);
|
||||
|
||||
// Stickers per user
|
||||
const userStickers = await TaskCompletion.aggregate([
|
||||
{ $match: { stickerId: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: { user: '$completedBy', sticker: '$stickerId' }, count: { $sum: 1 } } },
|
||||
{ $sort: { '_id.user': 1, count: -1 } }
|
||||
]);
|
||||
|
||||
// Recent completions (enough for ~3 months of calendar)
|
||||
const recentCompletions = await TaskCompletion.find()
|
||||
.sort({ completedAt: -1 })
|
||||
.limit(500)
|
||||
.lean();
|
||||
|
||||
return json({ userStats, userStickers, recentCompletions });
|
||||
};
|
||||
Reference in New Issue
Block a user