ubiquitous-invention/apps/web/lib/hooks/use-view-data.ts
Randall Stillwell a508ece6e7 feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:

- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)

Made-with: Cursor
2026-03-26 22:39:16 -05:00

108 lines
6 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
export type SortDirection = "asc" | "desc";
export interface ViewFilter {
field: string;
operator: "eq" | "neq" | "contains" | "gt" | "lt" | "in" | "isEmpty" | "isNotEmpty";
value: unknown;
}
export interface ViewSort {
field: string;
direction: SortDirection;
}
export interface ViewConfig {
filters: ViewFilter[];
sorts: ViewSort[];
groupBy: string | null;
}
export interface ViewObject {
id: string;
type: string;
title: string;
status: string | null;
icon: string | null;
sortOrder: number;
parentId: string | null;
createdAt: string;
updatedAt: string;
assignees?: { user: { id: string; name: string | null; avatarUrl: string | null } }[];
propertyValues?: { propertyDefinition: { id: string; name: string; fieldType: string }; value: unknown }[];
}
const MOCK_OBJECTS: ViewObject[] = [
{ id: "1", type: "task", title: "Design landing page mockups", status: "in_progress", icon: null, sortOrder: 0, parentId: null, createdAt: "2025-03-20", updatedAt: "2025-03-25", assignees: [{ user: { id: "u1", name: "Alice", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "High" }, { propertyDefinition: { id: "p2", name: "Due Date", fieldType: "date" }, value: "2025-04-01" }] },
{ id: "2", type: "task", title: "Implement authentication flow", status: "open", icon: null, sortOrder: 1, parentId: null, createdAt: "2025-03-21", updatedAt: "2025-03-24", assignees: [{ user: { id: "u2", name: "Bob", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "High" }] },
{ id: "3", type: "task", title: "Set up CI/CD pipeline", status: "done", icon: null, sortOrder: 2, parentId: null, createdAt: "2025-03-19", updatedAt: "2025-03-23", assignees: [{ user: { id: "u3", name: "Charlie", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "Medium" }] },
{ id: "4", type: "task", title: "Write API documentation", status: "open", icon: null, sortOrder: 3, parentId: null, createdAt: "2025-03-22", updatedAt: "2025-03-25", assignees: [], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "Low" }] },
{ id: "5", type: "task", title: "Database schema review", status: "in_progress", icon: null, sortOrder: 4, parentId: null, createdAt: "2025-03-18", updatedAt: "2025-03-25", assignees: [{ user: { id: "u1", name: "Alice", avatarUrl: null } }, { user: { id: "u2", name: "Bob", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "High" }, { propertyDefinition: { id: "p2", name: "Due Date", fieldType: "date" }, value: "2025-03-28" }] },
{ id: "6", type: "task", title: "User testing session", status: "open", icon: null, sortOrder: 5, parentId: null, createdAt: "2025-03-23", updatedAt: "2025-03-25", assignees: [{ user: { id: "u4", name: "Diana", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "Medium" }, { propertyDefinition: { id: "p2", name: "Due Date", fieldType: "date" }, value: "2025-04-05" }] },
{ id: "7", type: "task", title: "Performance optimization", status: "closed", icon: null, sortOrder: 6, parentId: null, createdAt: "2025-03-15", updatedAt: "2025-03-20", assignees: [{ user: { id: "u3", name: "Charlie", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "Low" }] },
{ id: "8", type: "task", title: "Mobile responsive fixes", status: "in_progress", icon: null, sortOrder: 7, parentId: null, createdAt: "2025-03-24", updatedAt: "2025-03-25", assignees: [{ user: { id: "u2", name: "Bob", avatarUrl: null } }], propertyValues: [{ propertyDefinition: { id: "p1", name: "Priority", fieldType: "select" }, value: "High" }] },
];
function applyFilters(objects: ViewObject[], filters: ViewFilter[]): ViewObject[] {
return objects.filter((obj) =>
filters.every((f) => {
const val = f.field === "status" ? obj.status : f.field === "title" ? obj.title : null;
if (f.operator === "eq") return val === f.value;
if (f.operator === "neq") return val !== f.value;
if (f.operator === "contains" && typeof val === "string" && typeof f.value === "string")
return val.toLowerCase().includes(f.value.toLowerCase());
if (f.operator === "isEmpty") return !val;
if (f.operator === "isNotEmpty") return !!val;
return true;
}),
);
}
function getField(obj: ViewObject, field: string): unknown {
return (obj as unknown as Record<string, unknown>)[field];
}
function applySorts(objects: ViewObject[], sorts: ViewSort[]): ViewObject[] {
if (sorts.length === 0) return objects;
return [...objects].sort((a, b) => {
for (const s of sorts) {
const aVal = getField(a, s.field) ?? "";
const bVal = getField(b, s.field) ?? "";
const cmp = String(aVal).localeCompare(String(bVal));
if (cmp !== 0) return s.direction === "asc" ? cmp : -cmp;
}
return 0;
});
}
function applyGroupBy(objects: ViewObject[], groupBy: string | null): Record<string, ViewObject[]> {
if (!groupBy) return { "All Items": objects };
const groups: Record<string, ViewObject[]> = {};
for (const obj of objects) {
const key = String(getField(obj, groupBy) ?? "No Value");
if (!groups[key]) groups[key] = [];
groups[key].push(obj);
}
return groups;
}
export function useViewData(config: ViewConfig) {
const [objects] = useState<ViewObject[]>(MOCK_OBJECTS);
const processed = useMemo(() => {
let result = applyFilters(objects, config.filters);
result = applySorts(result, config.sorts);
const grouped = applyGroupBy(result, config.groupBy);
return { items: result, grouped };
}, [objects, config]);
return {
items: processed.items,
grouped: processed.grouped,
isLoading: false,
total: processed.items.length,
};
}