Add a system for organizing tasks with customizable color-coded labels
Implement a label system with API endpoints for CRUD operations on labels and tasks, including schema definitions for labels and tasks with label associations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/XrG8SoT
This commit is contained in:
@@ -14,10 +14,6 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 39485
|
|
||||||
externalPort = 3001
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 41353
|
localPort = 41353
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
+129
-4
@@ -1,13 +1,138 @@
|
|||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { createServer, type Server } from "http";
|
import { createServer, type Server } from "http";
|
||||||
import { storage } from "./storage";
|
import { storage } from "./storage";
|
||||||
|
import { insertLabelSchema, insertTaskSchema } from "@shared/schema";
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// put application routes here
|
// Labels API routes
|
||||||
// prefix all routes with /api
|
app.get("/api/labels", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const labels = await storage.getAllLabels();
|
||||||
|
res.json(labels);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to fetch labels" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// use storage to perform CRUD operations on the storage interface
|
app.get("/api/labels/:id", async (req, res) => {
|
||||||
// e.g. storage.insertUser(user) or storage.getUserByUsername(username)
|
try {
|
||||||
|
const label = await storage.getLabel(req.params.id);
|
||||||
|
if (!label) {
|
||||||
|
return res.status(404).json({ error: "Label not found" });
|
||||||
|
}
|
||||||
|
res.json(label);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to fetch label" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/labels", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = insertLabelSchema.safeParse(req.body);
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(400).json({ error: "Invalid label data", details: result.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = await storage.createLabel(result.data);
|
||||||
|
res.status(201).json(label);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to create label" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/api/labels/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const updates = insertLabelSchema.partial().safeParse(req.body);
|
||||||
|
if (!updates.success) {
|
||||||
|
return res.status(400).json({ error: "Invalid label data", details: updates.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = await storage.updateLabel(req.params.id, updates.data);
|
||||||
|
if (!label) {
|
||||||
|
return res.status(404).json({ error: "Label not found" });
|
||||||
|
}
|
||||||
|
res.json(label);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to update label" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/labels/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const success = await storage.deleteLabel(req.params.id);
|
||||||
|
if (!success) {
|
||||||
|
return res.status(404).json({ error: "Label not found" });
|
||||||
|
}
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to delete label" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tasks API routes
|
||||||
|
app.get("/api/tasks", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tasks = await storage.getAllTasks();
|
||||||
|
res.json(tasks);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to fetch tasks" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/tasks/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const task = await storage.getTask(req.params.id);
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ error: "Task not found" });
|
||||||
|
}
|
||||||
|
res.json(task);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to fetch task" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/tasks", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = insertTaskSchema.safeParse(req.body);
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(400).json({ error: "Invalid task data", details: result.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await storage.createTask(result.data);
|
||||||
|
res.status(201).json(task);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to create task" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/api/tasks/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const updates = insertTaskSchema.partial().safeParse(req.body);
|
||||||
|
if (!updates.success) {
|
||||||
|
return res.status(400).json({ error: "Invalid task data", details: updates.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await storage.updateTask(req.params.id, updates.data);
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ error: "Task not found" });
|
||||||
|
}
|
||||||
|
res.json(task);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to update task" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/tasks/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const success = await storage.deleteTask(req.params.id);
|
||||||
|
if (!success) {
|
||||||
|
return res.status(404).json({ error: "Task not found" });
|
||||||
|
}
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Failed to delete task" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
|
|||||||
+105
-1
@@ -1,4 +1,4 @@
|
|||||||
import { type User, type InsertUser } from "@shared/schema";
|
import { type User, type InsertUser, type Label, type InsertLabel, type Task, type InsertTask } from "@shared/schema";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
// modify the interface with any CRUD methods
|
// modify the interface with any CRUD methods
|
||||||
@@ -8,13 +8,47 @@ export interface IStorage {
|
|||||||
getUser(id: string): Promise<User | undefined>;
|
getUser(id: string): Promise<User | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(username: string): Promise<User | undefined>;
|
||||||
createUser(user: InsertUser): Promise<User>;
|
createUser(user: InsertUser): Promise<User>;
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
getAllLabels(): Promise<Label[]>;
|
||||||
|
getLabel(id: string): Promise<Label | undefined>;
|
||||||
|
createLabel(label: InsertLabel): Promise<Label>;
|
||||||
|
updateLabel(id: string, updates: Partial<InsertLabel>): Promise<Label | undefined>;
|
||||||
|
deleteLabel(id: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
getAllTasks(): Promise<Task[]>;
|
||||||
|
getTask(id: string): Promise<Task | undefined>;
|
||||||
|
createTask(task: InsertTask): Promise<Task>;
|
||||||
|
updateTask(id: string, updates: Partial<InsertTask>): Promise<Task | undefined>;
|
||||||
|
deleteTask(id: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemStorage implements IStorage {
|
export class MemStorage implements IStorage {
|
||||||
private users: Map<string, User>;
|
private users: Map<string, User>;
|
||||||
|
private labels: Map<string, Label>;
|
||||||
|
private tasks: Map<string, Task>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.users = new Map();
|
this.users = new Map();
|
||||||
|
this.labels = new Map();
|
||||||
|
this.tasks = new Map();
|
||||||
|
|
||||||
|
// Create some default labels
|
||||||
|
this.createDefaultLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDefaultLabels() {
|
||||||
|
const defaultLabels = [
|
||||||
|
{ name: "Work", color: "#3B82F6" },
|
||||||
|
{ name: "Personal", color: "#10B981" },
|
||||||
|
{ name: "Urgent", color: "#EF4444" },
|
||||||
|
{ name: "Study", color: "#8B5CF6" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const labelData of defaultLabels) {
|
||||||
|
await this.createLabel(labelData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(id: string): Promise<User | undefined> {
|
async getUser(id: string): Promise<User | undefined> {
|
||||||
@@ -33,6 +67,76 @@ export class MemStorage implements IStorage {
|
|||||||
this.users.set(id, user);
|
this.users.set(id, user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
async getAllLabels(): Promise<Label[]> {
|
||||||
|
return Array.from(this.labels.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLabel(id: string): Promise<Label | undefined> {
|
||||||
|
return this.labels.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLabel(insertLabel: InsertLabel): Promise<Label> {
|
||||||
|
const id = randomUUID();
|
||||||
|
const label: Label = { ...insertLabel, id };
|
||||||
|
this.labels.set(id, label);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLabel(id: string, updates: Partial<InsertLabel>): Promise<Label | undefined> {
|
||||||
|
const existing = this.labels.get(id);
|
||||||
|
if (!existing) return undefined;
|
||||||
|
|
||||||
|
const updated: Label = { ...existing, ...updates };
|
||||||
|
this.labels.set(id, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLabel(id: string): Promise<boolean> {
|
||||||
|
return this.labels.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
async getAllTasks(): Promise<Task[]> {
|
||||||
|
return Array.from(this.tasks.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTask(id: string): Promise<Task | undefined> {
|
||||||
|
return this.tasks.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTask(insertTask: InsertTask): Promise<Task> {
|
||||||
|
const id = randomUUID();
|
||||||
|
const task: Task = {
|
||||||
|
id,
|
||||||
|
title: insertTask.title,
|
||||||
|
description: insertTask.description || null,
|
||||||
|
status: insertTask.status || "todo",
|
||||||
|
priority: insertTask.priority || "medium",
|
||||||
|
dueDate: insertTask.dueDate || null,
|
||||||
|
timeTracked: insertTask.timeTracked || 0,
|
||||||
|
isTracking: insertTask.isTracking || false,
|
||||||
|
projectId: insertTask.projectId || null,
|
||||||
|
notes: insertTask.notes || null,
|
||||||
|
labelId: insertTask.labelId || null,
|
||||||
|
};
|
||||||
|
this.tasks.set(id, task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(id: string, updates: Partial<InsertTask>): Promise<Task | undefined> {
|
||||||
|
const existing = this.tasks.get(id);
|
||||||
|
if (!existing) return undefined;
|
||||||
|
|
||||||
|
const updated: Task = { ...existing, ...updates };
|
||||||
|
this.tasks.set(id, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTask(id: string): Promise<boolean> {
|
||||||
|
return this.tasks.delete(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new MemStorage();
|
export const storage = new MemStorage();
|
||||||
|
|||||||
+33
-1
@@ -1,5 +1,5 @@
|
|||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
|
import { pgTable, text, varchar, timestamp, integer, boolean } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -9,10 +9,42 @@ export const users = pgTable("users", {
|
|||||||
password: text("password").notNull(),
|
password: text("password").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const labels = pgTable("labels", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
color: text("color").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tasks = pgTable("tasks", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
title: text("title").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
status: text("status").notNull().default("todo"), // 'todo' | 'inProgress' | 'done'
|
||||||
|
priority: text("priority").notNull().default("medium"), // 'low' | 'medium' | 'high'
|
||||||
|
dueDate: timestamp("due_date"),
|
||||||
|
timeTracked: integer("time_tracked").notNull().default(0), // in minutes
|
||||||
|
isTracking: boolean("is_tracking").notNull().default(false),
|
||||||
|
projectId: text("project_id"),
|
||||||
|
notes: text("notes"),
|
||||||
|
labelId: varchar("label_id").references(() => labels.id),
|
||||||
|
});
|
||||||
|
|
||||||
export const insertUserSchema = createInsertSchema(users).pick({
|
export const insertUserSchema = createInsertSchema(users).pick({
|
||||||
username: true,
|
username: true,
|
||||||
password: true,
|
password: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertLabelSchema = createInsertSchema(labels).omit({
|
||||||
|
id: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertTaskSchema = createInsertSchema(tasks).omit({
|
||||||
|
id: true,
|
||||||
|
});
|
||||||
|
|
||||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
|
export type InsertLabel = z.infer<typeof insertLabelSchema>;
|
||||||
|
export type Label = typeof labels.$inferSelect;
|
||||||
|
export type InsertTask = z.infer<typeof insertTaskSchema>;
|
||||||
|
export type Task = typeof tasks.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user