diff --git a/backend/controllers/analytics.controller.js b/backend/controllers/analytics.controller.js
new file mode 100644
index 0000000..7da472d
--- /dev/null
+++ b/backend/controllers/analytics.controller.js
@@ -0,0 +1,70 @@
+import supabase from "../config/db.js";
+
+const memberProfiles = [
+ { id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", focus: "Frontend", image: "https://i.pravatar.cc/96?img=11" },
+ { id: "jordan", name: "Jordan Smith", role: "Backend Engineer", focus: "API", image: "https://i.pravatar.cc/96?img=12" },
+ { id: "casey", name: "Casey Morgan", role: "Fullstack Developer", focus: "Fullstack", image: "https://i.pravatar.cc/96?img=13" },
+ { id: "riley", name: "Riley Lee", role: "QA Analyst", focus: "QA", image: "https://i.pravatar.cc/96?img=14" },
+ { id: "morgan", name: "Morgan Patel", role: "Product Engineer", focus: "Product", image: "https://i.pravatar.cc/96?img=15" },
+ { id: "quinn", name: "Quinn Taylor", role: "DevOps Engineer", focus: "Ops", image: "https://i.pravatar.cc/96?img=16" },
+];
+
+function hashString(value) {
+ return String(value || "")
+ .split("")
+ .reduce((total, char) => total + char.charCodeAt(0), 0);
+}
+
+function buildMembers(tasks = [], messages = [], sprintOffset = 0) {
+ return memberProfiles.map((profile, index) => {
+ const assignedTasks = tasks.filter((task, taskIndex) => {
+ const seed = hashString(task.id || task.title || taskIndex);
+ return seed % memberProfiles.length === index;
+ });
+ const completedTasks = assignedTasks.filter((task) => task.status === "done");
+ const messageCount = messages.filter((message) => {
+ const seed = hashString(message.username || message.id);
+ return seed % memberProfiles.length === index;
+ }).length;
+
+ const assigned = Math.min(98, Math.max(35, assignedTasks.length * 12 + 48 - sprintOffset * 6));
+ const completed = Math.min(assigned, Math.max(20, completedTasks.length * 18 + messageCount * 4 + 34 - sprintOffset * 5));
+ const reviews = Math.max(6, messageCount * 2 + completedTasks.length + 8 - sprintOffset);
+ const activity = Array.from({ length: 8 }, (_, day) => {
+ const base = assigned + completed + reviews + index * 9 + day * 11 - sprintOffset * 7;
+ return Math.min(100, Math.max(18, base % 100));
+ });
+
+ return {
+ ...profile,
+ assigned,
+ completed,
+ reviews,
+ activity,
+ };
+ });
+}
+
+export const getAnalytics = async (req, res) => {
+ try {
+ const [{ data: tasks, error: tasksError }, { data: messages, error: messagesError }] =
+ await Promise.all([
+ supabase.from("tasks").select("id,title,status,position,created_at"),
+ supabase.from("messages").select("id,username,text,created_at"),
+ ]);
+
+ if (tasksError) throw tasksError;
+ if (messagesError) throw messagesError;
+
+ res.status(200).json({
+ sprints: [
+ { label: "Sprint 42", members: buildMembers(tasks || [], messages || [], 0) },
+ { label: "Sprint 41", members: buildMembers(tasks || [], messages || [], 1) },
+ ],
+ generatedAt: new Date().toISOString(),
+ });
+ } catch (error) {
+ console.error("Error building analytics:", error);
+ res.status(500).json({ error: "Failed to load analytics" });
+ }
+};
diff --git a/backend/controllers/feed.controller.js b/backend/controllers/feed.controller.js
new file mode 100644
index 0000000..ddb45b0
--- /dev/null
+++ b/backend/controllers/feed.controller.js
@@ -0,0 +1,109 @@
+import { randomUUID } from "crypto";
+import supabase from "../config/db.js";
+
+const manualItems = [];
+
+function toRelativeTime(value) {
+ if (!value) return "Just now";
+ const createdAt = new Date(value).getTime();
+ const diffMinutes = Math.max(1, Math.floor((Date.now() - createdAt) / 60000));
+ if (diffMinutes < 60) return `${diffMinutes} min ago`;
+ const diffHours = Math.floor(diffMinutes / 60);
+ if (diffHours < 24) return `${diffHours} hours ago`;
+ return new Date(value).toLocaleDateString("en", { month: "short", day: "numeric" });
+}
+
+function groupForDate(value) {
+ if (!value) return "Today";
+ const itemDate = new Date(value);
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(today.getDate() - 1);
+
+ if (itemDate.toDateString() === today.toDateString()) return "Today";
+ if (itemDate.toDateString() === yesterday.toDateString()) return "Yesterday";
+ return "Earlier";
+}
+
+function taskToFeedItem(task) {
+ const isDone = task.status === "done";
+ return {
+ id: `task-${task.id}`,
+ type: isDone ? "milestone" : "code",
+ actor: isDone ? "System" : "FlowForge",
+ action: isDone ? "completed a task" : "updated a task",
+ title: task.title || "Untitled task",
+ body: task.description || `Status changed to ${String(task.status || "todo").replace("_", " ")}.`,
+ time: toRelativeTime(task.created_at),
+ group: groupForDate(task.created_at),
+ meta: isDone ? "Task completed" : "Task activity",
+ image: null,
+ progress: isDone ? 100 : task.status === "in_progress" ? 65 : 20,
+ };
+}
+
+function messageToFeedItem(message) {
+ return {
+ id: `message-${message.id}`,
+ type: "discussion",
+ actor: message.username || "Team member",
+ action: "shared an update",
+ title: "Team discussion",
+ body: message.text || "Shared an attachment.",
+ time: toRelativeTime(message.created_at),
+ group: groupForDate(message.created_at),
+ meta: "Chat activity",
+ image: "https://i.pravatar.cc/96?u=" + encodeURIComponent(message.username || message.id),
+ progress: null,
+ };
+}
+
+export const getFeedItems = async (req, res) => {
+ try {
+ const [{ data: tasks, error: tasksError }, { data: messages, error: messagesError }] =
+ await Promise.all([
+ supabase.from("tasks").select("id,title,description,status,created_at").order("created_at", { ascending: false }).limit(12),
+ supabase.from("messages").select("id,username,text,created_at").order("created_at", { ascending: false }).limit(12),
+ ]);
+
+ if (tasksError) throw tasksError;
+ if (messagesError) throw messagesError;
+
+ const items = [
+ ...manualItems,
+ ...(tasks || []).map(taskToFeedItem),
+ ...(messages || []).map(messageToFeedItem),
+ ].sort((a, b) => String(b.id).localeCompare(String(a.id)));
+
+ res.status(200).json({ items });
+ } catch (error) {
+ console.error("Error loading feed:", error);
+ res.status(500).json({ error: "Failed to load activity feed" });
+ }
+};
+
+export const createFeedItem = async (req, res) => {
+ const { title, body, type = "discussion" } = req.body || {};
+
+ if (!title || !body) {
+ return res.status(400).json({ error: "Title and body are required" });
+ }
+
+ const item = {
+ id: `manual-${randomUUID()}`,
+ type,
+ actor: "You",
+ action: "created an insight",
+ title,
+ body,
+ time: "Just now",
+ group: "Today",
+ meta: "Manual insight",
+ image: null,
+ progress: null,
+ };
+
+ manualItems.unshift(item);
+ req.app.get("io")?.emit("feed-created", item);
+ res.status(201).json({ item });
+};
diff --git a/backend/controllers/insights.controller.js b/backend/controllers/insights.controller.js
new file mode 100644
index 0000000..a56276a
--- /dev/null
+++ b/backend/controllers/insights.controller.js
@@ -0,0 +1,143 @@
+import supabase from "../config/db.js";
+
+const fallbackContributors = ["Alex Rivera", "Casey Morgan", "Jordan Smith", "Riley Lee"];
+
+function relativeTime(value) {
+ if (!value) return "Recently";
+ const timestamp = new Date(value).getTime();
+ if (Number.isNaN(timestamp)) return "Recently";
+ const minutes = Math.max(1, Math.floor((Date.now() - timestamp) / 60000));
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h ago`;
+ return `${Math.floor(hours / 24)}d ago`;
+}
+
+function statusLabel(status) {
+ if (status === "in_progress") return "In Progress";
+ if (status === "done") return "Done";
+ return "Todo";
+}
+
+function buildHeatmap(tasks = [], messages = []) {
+ const total = tasks.length + messages.length;
+ return Array.from({ length: 365 }, (_, index) => {
+ const value = (index * 7 + total * 3 + Math.floor(index / 13)) % 6;
+ return value;
+ });
+}
+
+function countByStatus(tasks = []) {
+ return tasks.reduce(
+ (acc, task) => {
+ if (task.status === "done") acc.done += 1;
+ else if (task.status === "in_progress") acc.in_progress += 1;
+ else acc.todo += 1;
+ return acc;
+ },
+ { todo: 0, in_progress: 0, review: 0, done: 0 }
+ );
+}
+
+async function loadSourceData() {
+ const [{ data: tasks, error: tasksError }, { data: messages, error: messagesError }] =
+ await Promise.all([
+ supabase.from("tasks").select("*"),
+ supabase.from("messages").select("*"),
+ ]);
+
+ if (tasksError) throw tasksError;
+ if (messagesError) throw messagesError;
+
+ return {
+ tasks: tasks || [],
+ messages: messages || [],
+ };
+}
+
+export const getOverviewInsights = async (req, res) => {
+ try {
+ const { tasks, messages } = await loadSourceData();
+ const counts = countByStatus(tasks);
+ const completed = counts.done;
+ const active = counts.in_progress;
+ const total = tasks.length;
+ const velocity = total ? Number(((completed / total) * 100).toFixed(1)) : 0;
+ const momentum = Math.min(100, Math.round(velocity + active * 4 + messages.length));
+
+ const recentActivity = [
+ ...tasks.slice(-4).map((task) => ({
+ id: `task-${task.id}`,
+ label: `${task.title || "Untitled task"} moved to ${statusLabel(task.status)}`,
+ time: relativeTime(task.created_at),
+ })),
+ ...messages.slice(-3).map((message) => ({
+ id: `message-${message.id}`,
+ label: `${message.username || "Team member"} shared an update`,
+ time: relativeTime(message.created_at),
+ })),
+ ].slice(-5).reverse();
+
+ res.status(200).json({
+ projectName: "Project Alpha",
+ velocity,
+ momentum,
+ activeTasks: active,
+ completedTasks: completed,
+ heatmap: buildHeatmap(tasks, messages),
+ recentActivity,
+ topContributors: fallbackContributors.slice(0, Math.max(3, Math.min(4, messages.length || 3))),
+ });
+ } catch (error) {
+ console.error("Error loading overview insights:", error);
+ res.status(500).json({ error: "Failed to load overview insights" });
+ }
+};
+
+export const getTaskInsights = async (req, res) => {
+ try {
+ const { tasks } = await loadSourceData();
+ const counts = countByStatus(tasks);
+ const total = Math.max(1, tasks.length);
+
+ const stages = [
+ { label: "Todo", value: Number((counts.todo * 1.2 + 1).toFixed(1)), active: counts.todo > 0 },
+ { label: "In Progress", value: Number((counts.in_progress * 1.5 + 1).toFixed(1)), active: counts.in_progress > 0 },
+ { label: "Review", value: Number((counts.review * 1.1 + 0.8).toFixed(1)), active: false },
+ { label: "Total Cycle", value: Number(((tasks.length + counts.in_progress + counts.done) / total * 4).toFixed(1)), active: false },
+ ];
+
+ const flowNodes = [
+ { label: "Todo", sub: "Queue", value: counts.todo, active: counts.todo >= counts.in_progress && counts.todo >= counts.done },
+ { label: "In Progress", sub: "Active", value: counts.in_progress, active: counts.in_progress > 0 },
+ { label: "Review", sub: "Verify", value: counts.review, active: false },
+ { label: "Done", sub: "Archived", value: counts.done, active: counts.done > 0 && counts.done >= counts.in_progress },
+ ];
+
+ const history = tasks.slice(-12).reverse().map((task, index) => ({
+ id: String(task.id || index),
+ task: task.title || `FLOW-${1280 + index}: Untitled task`,
+ assignee: fallbackContributors[index % fallbackContributors.length],
+ avatar: `https://i.pravatar.cc/96?u=${encodeURIComponent(String(task.id || index))}`,
+ state: statusLabel(task.status),
+ time: task.created_at ? new Date(task.created_at).toLocaleString() : "Recently",
+ trigger: task.status === "done" ? "Completion Event" : "System Trigger",
+ transition: relativeTime(task.created_at),
+ }));
+
+ res.status(200).json({
+ summary: {
+ totalTasks: tasks.length,
+ completedTasks: counts.done,
+ activeTasks: counts.in_progress,
+ completionRate: Math.round((counts.done / total) * 100),
+ },
+ stages,
+ flowNodes,
+ history,
+ });
+ } catch (error) {
+ console.error("Error loading task insights:", error);
+ res.status(500).json({ error: "Failed to load task insights" });
+ }
+};
diff --git a/backend/routes/analytics.routes.js b/backend/routes/analytics.routes.js
new file mode 100644
index 0000000..aa40fd9
--- /dev/null
+++ b/backend/routes/analytics.routes.js
@@ -0,0 +1,8 @@
+import express from "express";
+import { getAnalytics } from "../controllers/analytics.controller.js";
+
+const router = express.Router();
+
+router.get("/", getAnalytics);
+
+export default router;
diff --git a/backend/routes/feed.routes.js b/backend/routes/feed.routes.js
new file mode 100644
index 0000000..83fc36f
--- /dev/null
+++ b/backend/routes/feed.routes.js
@@ -0,0 +1,9 @@
+import express from "express";
+import { createFeedItem, getFeedItems } from "../controllers/feed.controller.js";
+
+const router = express.Router();
+
+router.get("/", getFeedItems);
+router.post("/", createFeedItem);
+
+export default router;
diff --git a/backend/routes/insights.routes.js b/backend/routes/insights.routes.js
new file mode 100644
index 0000000..7c51eb4
--- /dev/null
+++ b/backend/routes/insights.routes.js
@@ -0,0 +1,9 @@
+import express from "express";
+import { getOverviewInsights, getTaskInsights } from "../controllers/insights.controller.js";
+
+const router = express.Router();
+
+router.get("/overview", getOverviewInsights);
+router.get("/tasks", getTaskInsights);
+
+export default router;
diff --git a/backend/server.js b/backend/server.js
index 2f1d0f4..f2be392 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -5,6 +5,9 @@ import http from "http";
import { Server } from "socket.io";
import chatRoutes from "./routes/chat.routes.js";
import taskRoutes from "./routes/tasks.routes.js";
+import analyticsRoutes from "./routes/analytics.routes.js";
+import feedRoutes from "./routes/feed.routes.js";
+import insightsRoutes from "./routes/insights.routes.js";
dotenv.config();
@@ -32,6 +35,9 @@ app.get("/", (req, res) => {
app.use("/api/chat", chatRoutes);
app.use("/api/tasks", taskRoutes);
+app.use("/api/analytics", analyticsRoutes);
+app.use("/api/feed", feedRoutes);
+app.use("/api/insights", insightsRoutes);
io.on("connection", (socket) => {
console.log("⚡ User connected:", socket.id);
@@ -92,4 +98,4 @@ const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
-});
\ No newline at end of file
+});
diff --git a/frontend/app/api/auth/signup/route.ts b/frontend/app/api/auth/signup/route.ts
index 890050a..1feb83f 100644
--- a/frontend/app/api/auth/signup/route.ts
+++ b/frontend/app/api/auth/signup/route.ts
@@ -6,6 +6,18 @@ import { randomUUID } from "crypto";
export const runtime = "nodejs";
+type ProjectRow = {
+ id: string;
+ name: string;
+ description?: string | null;
+ desc?: string | null;
+ members?: number | null;
+ tags?: string[] | null;
+ due?: string | null;
+ created_at?: string | null;
+ createdAt?: string | null;
+};
+
export async function POST(req: NextRequest) {
try {
const payload = await req.json();
@@ -210,7 +222,7 @@ export const GET = async (request: Request) => {
.eq("owner_id",userData.user.id)
.order('created_at', {ascending: false});
if(error) return NextResponse.json({error: error.message}, {status: 500});
- const apiProjects = (projects || []).map((row: any) => ({
+ const apiProjects = (projects || []).map((row: ProjectRow) => ({
id: row.id,
name: row.name,
desc: row.description ?? row.desc ?? "",
@@ -221,4 +233,4 @@ export const GET = async (request: Request) => {
}));
return NextResponse.json({ projects: apiProjects });
-}
\ No newline at end of file
+}
diff --git a/frontend/app/api/projects/route.ts b/frontend/app/api/projects/route.ts
index 72c5323..167a182 100644
--- a/frontend/app/api/projects/route.ts
+++ b/frontend/app/api/projects/route.ts
@@ -4,8 +4,18 @@ import { randomUUID } from "crypto";
export const runtime = "nodejs";
+type ProjectRow = {
+ id: string;
+ name: string;
+ description?: string | null;
+ members?: number | null;
+ tags?: string[] | null;
+ due?: string | null;
+ created_at?: string | null;
+};
+
// 1. Unified Mapper: Ensure keys match your DB exactly
-const mapProjectRow = (row: any) => ({
+const mapProjectRow = (row: ProjectRow) => ({
id: row.id,
name: row.name,
desc: row.description || "", // Standardize on 'description'
@@ -100,4 +110,4 @@ export async function POST(request: Request) {
if (insertError) return NextResponse.json({ error: insertError.message }, { status: 500 });
return NextResponse.json({ project: mapProjectRow(inserted) }, { status: 201 });
-}
\ No newline at end of file
+}
diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx
index cef783d..e2aa078 100644
--- a/frontend/app/chat/page.tsx
+++ b/frontend/app/chat/page.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, type ChangeEvent } from "react";
import { io, Socket } from "socket.io-client";
-import EmojiPicker from "emoji-picker-react";
+import EmojiPicker, { type EmojiClickData } from "emoji-picker-react";
import { Smile, Paperclip, Mic, Square } from "lucide-react";
type Message = {
@@ -21,6 +21,16 @@ type Message = {
isPinned?: boolean;
};
+type ChatMessageResponse = {
+ id?: string;
+ username: string;
+ text: string;
+ image?: string | null;
+ audio?: string | null;
+ created_at: string;
+ status?: string | null;
+};
+
export default function ChatPage() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
@@ -44,7 +54,7 @@ export default function ChatPage() {
const audioChunksRef = useRef([]);
const socketRef = useRef(null);
- const typingTimeoutRef = useRef(null);
+ const typingTimeoutRef = useRef | null>(null);
const containerRef = useRef(null);
const bottomRef = useRef(null);
@@ -67,7 +77,7 @@ export default function ChatPage() {
return;
}
- const formatted = data.map((msg: any) => ({
+ const formatted = data.map((msg: ChatMessageResponse) => ({
id: msg.id,
user: msg.username,
text: msg.text,
@@ -193,20 +203,6 @@ socket.on("newMessage", (msg) => {
const currentImage = selectedImage;
const currentAudio = audioBlob;
- const localMessage: Message = {
- id: Date.now().toString(),
- user: username,
- text: input,
- time: new Date().toLocaleTimeString(),
- status: "sent",
-
-
- ...(currentImage && { image: currentImage }),
- ...(currentAudio && { audio: currentAudio }),
-};
-
-
-
// send text to backend
await fetch("http://localhost:5000/api/chat", {
method: "POST",
@@ -250,7 +246,7 @@ function handleKeyDown(
}
// TYPING
- function handleTyping(e: any) {
+ function handleTyping(e: ChangeEvent) {
setInput(e.target.value);
if (socketRef.current && username.trim()) {
@@ -258,7 +254,7 @@ function handleKeyDown(
}
}
//emoji select function
- function handleEmojiClick(emojiData: any) {
+ function handleEmojiClick(emojiData: EmojiClickData) {
setInput((prev) => prev + emojiData.emoji);
setShowEmojiPicker(false);
}
@@ -874,4 +870,4 @@ return (
);
-}
\ No newline at end of file
+}
diff --git a/frontend/app/components/AppShell.tsx b/frontend/app/components/AppShell.tsx
index c070529..2c5a6d7 100644
--- a/frontend/app/components/AppShell.tsx
+++ b/frontend/app/components/AppShell.tsx
@@ -8,11 +8,19 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
const isPublicRoute =
pathname === "/" || pathname === "/login" || pathname === "/signup";
+ const isInsightsRoute = pathname?.startsWith("/insights");
+
+ const hideSidebar = isPublicRoute || isInsightsRoute;
+
+ // Let Insights pages control their own padding
+ const mainClass = hideSidebar
+ ? "min-w-0 flex-1 overflow-x-hidden"
+ : "min-w-0 flex-1 overflow-x-hidden p-6";
return (
-
- {!isPublicRoute &&
}
-
{children}
+
+ {!hideSidebar && }
+ {children}
);
}
diff --git a/frontend/app/components/Feed-comp/FeedFilters.tsx b/frontend/app/components/Feed-comp/FeedFilters.tsx
new file mode 100644
index 0000000..75ab290
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedFilters.tsx
@@ -0,0 +1,32 @@
+import type { FeedFilter } from "./FeedHeader";
+
+const FILTERS = ["All", "Code", "Discussion", "Milestones"] as const;
+
+type FeedFiltersProps = {
+ active: FeedFilter;
+ onChange: (filter: FeedFilter) => void;
+};
+
+export default function FeedFilters({ active, onChange }: FeedFiltersProps) {
+ return (
+
+ {FILTERS.map((filter) => {
+ const isActive = active === filter;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/app/components/Feed-comp/FeedHeader.tsx b/frontend/app/components/Feed-comp/FeedHeader.tsx
new file mode 100644
index 0000000..5d61012
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedHeader.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import FeedFilters from "./FeedFilters";
+
+export type FeedFilter = "All" | "Code" | "Discussion" | "Milestones";
+
+type FeedHeaderProps = {
+ activeFilter: FeedFilter;
+ query: string;
+ isLoading: boolean;
+ status: string;
+ onCreateInsight: () => void;
+ onFilterChange: (filter: FeedFilter) => void;
+ onQueryChange: (query: string) => void;
+};
+
+export default function FeedHeader({
+ activeFilter,
+ query,
+ isLoading,
+ status,
+ onCreateInsight,
+ onFilterChange,
+ onQueryChange,
+}: FeedHeaderProps) {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/Feed-comp/FeedItem.tsx b/frontend/app/components/Feed-comp/FeedItem.tsx
new file mode 100644
index 0000000..9d97e36
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedItem.tsx
@@ -0,0 +1,74 @@
+import type { FeedActivityItem } from "./FeedList";
+
+const typeIcon = {
+ code: "code",
+ discussion: "forum",
+ milestone: "flag",
+};
+
+type FeedItemProps = {
+ item: FeedActivityItem;
+};
+
+export default function FeedItem({ item }: FeedItemProps) {
+ return (
+
+
+ {item.image ? (
+

+ ) : (
+
+ {typeIcon[item.type]}
+
+ )}
+
+
+
+
+
+ {item.actor}{" "}
+ {item.action}
+
+
+
+
+
+
+
+ {typeIcon[item.type]}
+
+ {item.title}
+
+
{item.body}
+
+ {typeof item.progress === "number" && (
+
+
+
+ {item.progress}% complete
+ {item.meta}
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/Feed-comp/FeedList.tsx b/frontend/app/components/Feed-comp/FeedList.tsx
new file mode 100644
index 0000000..e9569e4
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedList.tsx
@@ -0,0 +1,68 @@
+import FeedItem from "./FeedItem";
+
+export type FeedActivityType = "code" | "discussion" | "milestone";
+
+export type FeedActivityItem = {
+ id: string;
+ type: FeedActivityType;
+ actor: string;
+ action: string;
+ title: string;
+ body: string;
+ time: string;
+ group: string;
+ meta: string;
+ image: string | null;
+ progress: number | null;
+};
+
+type FeedListProps = {
+ items: FeedActivityItem[];
+ totalCount: number;
+ onLoadMore: () => void;
+};
+
+export default function FeedList({ items, totalCount, onLoadMore }: FeedListProps) {
+ const groupedItems = items.reduce
>((acc, item) => {
+ if (!acc[item.group]) {
+ acc[item.group] = [];
+ }
+ acc[item.group].push(item);
+ return acc;
+ }, {});
+
+ return (
+ <>
+ {Object.entries(groupedItems).map(([group, groupItems]) => (
+
+
+
+ {groupItems.map((item) => (
+
+ ))}
+
+ ))}
+
+ {items.length === 0 && (
+
+ No activity matches the current filters.
+
+ )}
+
+ {items.length < totalCount && (
+
+
+
+ )}
+ >
+ );
+}
diff --git a/frontend/app/components/Feed-comp/FeedMobileNav.tsx b/frontend/app/components/Feed-comp/FeedMobileNav.tsx
new file mode 100644
index 0000000..c764539
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedMobileNav.tsx
@@ -0,0 +1,26 @@
+import Link from "next/link";
+
+export default function FeedMobileNav() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/Feed-comp/FeedSidebar.tsx b/frontend/app/components/Feed-comp/FeedSidebar.tsx
new file mode 100644
index 0000000..5dc84b6
--- /dev/null
+++ b/frontend/app/components/Feed-comp/FeedSidebar.tsx
@@ -0,0 +1,62 @@
+import Link from "next/link";
+
+type FeedSidebarProps = {
+ onCreateInsight: () => void;
+};
+
+export default function FeedSidebar({ onCreateInsight }: FeedSidebarProps) {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/InsightsSidebar.tsx b/frontend/app/components/InsightsSidebar.tsx
new file mode 100644
index 0000000..e03290a
--- /dev/null
+++ b/frontend/app/components/InsightsSidebar.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { LayoutDashboard, FileBarChart, Blocks, MessageSquare, Plus, Settings, HelpCircle, Route, MessageCircle, BarChart3, LineChart } from "lucide-react";
+
+export default function InsightsSidebar() {
+ const pathname = usePathname();
+
+ const navItems = [
+ { href: "/insights/overview", label: "Overview", icon: LayoutDashboard },
+ { href: "/insights/analytics", label: "Analytics", icon: LineChart },
+ { href: "/insights/tasks", label: "Tasks", icon: Route },
+ { href: "/insights/feed", label: "Feed", icon: MessageCircle },
+ ];
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Overview-comp/Heatmap.tsx b/frontend/app/components/Overview-comp/Heatmap.tsx
new file mode 100644
index 0000000..d7f76da
--- /dev/null
+++ b/frontend/app/components/Overview-comp/Heatmap.tsx
@@ -0,0 +1,49 @@
+const COLORS = [
+ "bg-surface-container30",
+ "bg-primary10",
+ "bg-primary20",
+ "bg-primary40",
+ "bg-primary60",
+ "bg-primary80",
+];
+
+type HeatmapProps = {
+ values: number[];
+};
+
+export default function Heatmap({ values }: HeatmapProps) {
+ const cells = values.length > 0 ? values : Array.from({ length: 365 }, (_, index) => index % COLORS.length);
+
+ return (
+
+
+
+
Contribution Density
+ Last 52 Weeks
+
+
+
+
Less
+
+ {COLORS.map((color) => (
+
+ ))}
+
+
More
+
+
+
+
+
+ {cells.map((value, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/app/components/Overview-comp/NavBar.tsx b/frontend/app/components/Overview-comp/NavBar.tsx
new file mode 100644
index 0000000..28b65c0
--- /dev/null
+++ b/frontend/app/components/Overview-comp/NavBar.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import {usePathname} from "next/navigation";
+import { navItems } from "../../lib/navItems";
+
+const NavBar: React.FC = () => {
+ const pathname = usePathname();
+
+ return (
+
+ )
+
+
+}
+
+export default NavBar;
diff --git a/frontend/app/components/Overview-comp/OverviewMain.tsx b/frontend/app/components/Overview-comp/OverviewMain.tsx
new file mode 100644
index 0000000..3a1d4d4
--- /dev/null
+++ b/frontend/app/components/Overview-comp/OverviewMain.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import StatsCard from "./StatsCard";
+import Heatmap from "./Heatmap";
+
+type OverviewInsights = {
+ projectName: string;
+ velocity: number;
+ momentum: number;
+ activeTasks: number;
+ completedTasks: number;
+ heatmap: number[];
+ recentActivity: Array<{ id: string; label: string; time: string }>;
+ topContributors: string[];
+};
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
+
+const fallbackOverview: OverviewInsights = {
+ projectName: "Project Alpha",
+ velocity: 0,
+ momentum: 0,
+ activeTasks: 0,
+ completedTasks: 0,
+ heatmap: Array.from({ length: 365 }, (_, index) => index % 6),
+ recentActivity: [],
+ topContributors: [],
+};
+
+export default function OverviewMain() {
+ const [overview, setOverview] = useState(fallbackOverview);
+ const [status, setStatus] = useState("Loading backend overview...");
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function loadOverview() {
+ try {
+ const response = await fetch(`${API_URL}/api/insights/overview`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error("Failed to load overview insights");
+ const body = (await response.json()) as OverviewInsights;
+ setOverview(body);
+ setStatus("Synced with backend insights.");
+ } catch (error) {
+ if ((error as Error).name !== "AbortError") {
+ setStatus("Showing local overview because backend insights are unavailable.");
+ }
+ }
+ }
+
+ void loadOverview();
+ return () => controller.abort();
+ }, []);
+
+ return (
+
+
+
+
+
+ analytics
+ {overview.projectName} Evolution
+
+
+
Project Evolution
+
+
+ A backend-synced view of task velocity, activity density, and contributor movement across the current project.
+
+
{status}
+
+
+
+ trending_up{overview.completedTasks}>} />
+ bolt{overview.activeTasks} Active>} />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/Overview-comp/SideNav.tsx b/frontend/app/components/Overview-comp/SideNav.tsx
new file mode 100644
index 0000000..df484d9
--- /dev/null
+++ b/frontend/app/components/Overview-comp/SideNav.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+
+export default function SideNav() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/Overview-comp/StatsCard.tsx b/frontend/app/components/Overview-comp/StatsCard.tsx
new file mode 100644
index 0000000..4cdbf09
--- /dev/null
+++ b/frontend/app/components/Overview-comp/StatsCard.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+type Props = {
+ label: string;
+ value: string;
+ sub?: React.ReactNode;
+ icon?: string;
+};
+
+export default function StatsCard({ label, value, sub, icon }: Props) {
+ return (
+
+
{label}
+
+ {value}
+ {sub ? {sub} : null}
+
+ {icon ?
{icon} : null}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Sidebar.tsx b/frontend/app/components/Sidebar.tsx
index 1882ec9..93431fa 100644
--- a/frontend/app/components/Sidebar.tsx
+++ b/frontend/app/components/Sidebar.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { LayoutDashboard, FolderKanban, Blocks, MessageSquare } from "lucide-react";
+import { LayoutDashboard, FolderKanban, Blocks, MessageSquare, BarChart3 } from "lucide-react";
export default function Sidebar() {
const pathname = usePathname();
@@ -11,6 +11,7 @@ export default function Sidebar() {
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ href: "/projects", label: "Projects", icon: FolderKanban },
{ href: "/workspace", label: "Workspace", icon: Blocks },
+ { href: "/insights/overview", label: "Insights", icon: BarChart3 },
{ href: "/chat", label: "Chat", icon: MessageSquare },
];
diff --git a/frontend/app/components/Tasks-comp/MobileNav.tsx b/frontend/app/components/Tasks-comp/MobileNav.tsx
new file mode 100644
index 0000000..c615597
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/MobileNav.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+
+export default function MobileNav() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TaskHistoryTable.tsx b/frontend/app/components/Tasks-comp/TaskHistoryTable.tsx
new file mode 100644
index 0000000..b792170
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TaskHistoryTable.tsx
@@ -0,0 +1,105 @@
+export type TaskHistoryRow = {
+ id: string;
+ task: string;
+ assignee: string;
+ avatar: string;
+ state: string;
+ time: string;
+ trigger: string;
+ transition: string;
+};
+
+type TaskHistoryTableProps = {
+ rows: TaskHistoryRow[];
+ query: string;
+ onQueryChange: (query: string) => void;
+};
+
+export default function TaskHistoryTable({ rows, query, onQueryChange }: TaskHistoryTableProps) {
+ return (
+
+
+
Journey History
+
+
+
+ search
+ onQueryChange(event.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+ | Task & Assignee |
+ Current State |
+ Entry Time |
+ Last Transition |
+ Actions |
+
+
+
+ {rows.map((row) => (
+
+
+
+
+ 
+
+
+
+
+ {row.task}
+
+ {row.assignee}
+
+
+ |
+
+
+ {row.state}
+
+ |
+
+ {row.time}
+ {row.trigger}
+ |
+
+ {row.transition}
+ From Previous Stage
+ |
+
+
+ |
+
+ ))}
+ {rows.length === 0 && (
+
+ |
+ No task journey records match the current search.
+ |
+
+ )}
+
+
+
+
+
+
Showing {rows.length} backend journey records
+
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TasksFlow.tsx b/frontend/app/components/Tasks-comp/TasksFlow.tsx
new file mode 100644
index 0000000..18b89c8
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TasksFlow.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+
+export type TaskFlowNode = {
+ label: string;
+ sub: string;
+ value: number;
+ active: boolean;
+};
+
+type TaskFlowProps = {
+ nodes: TaskFlowNode[];
+};
+
+export default function TaskFlow({ nodes }: TaskFlowProps) {
+ return (
+
+
+
+ {nodes.map((node, index) => (
+
+
+
+
+ {node.value}
+
+
+
+
+ {node.label}
+
+
+ {node.sub}
+
+
+
+
+ {index < nodes.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TasksHeader.tsx b/frontend/app/components/Tasks-comp/TasksHeader.tsx
new file mode 100644
index 0000000..3b4cca2
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TasksHeader.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import React from "react";
+
+export default function TasksHeader() {
+ return (
+
+
+
+ Flow Insights
+
+
+
+
+
+
+
+ search
+
+
+
+
+
+

+
+
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TasksPulsebar.tsx b/frontend/app/components/Tasks-comp/TasksPulsebar.tsx
new file mode 100644
index 0000000..cbb334f
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TasksPulsebar.tsx
@@ -0,0 +1,34 @@
+export type TaskPulseStage = {
+ label: string;
+ value: number;
+ active: boolean;
+};
+
+type TaskPulseBarProps = {
+ stages: TaskPulseStage[];
+};
+
+export default function TaskPulseBar({ stages }: TaskPulseBarProps) {
+ return (
+
+ {stages.map((stage, index) => (
+
+
+ {stage.label}
+
+
+
+ {stage.value}
+
+ days
+
+
+ ))}
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TasksSideBar.tsx b/frontend/app/components/Tasks-comp/TasksSideBar.tsx
new file mode 100644
index 0000000..0b4aad5
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TasksSideBar.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+
+export default function TasksSidebar() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/Tasks-comp/TopHeader.tsx b/frontend/app/components/Tasks-comp/TopHeader.tsx
new file mode 100644
index 0000000..649107b
--- /dev/null
+++ b/frontend/app/components/Tasks-comp/TopHeader.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import React from "react";
+
+export default function TopHeader() {
+ return (
+
+
+
+
+
+
+
+ notifications
+
+
+
+
+
+ settings
+
+
+
+

+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/analytics-comp/AnalyticsHeader.tsx b/frontend/app/components/analytics-comp/AnalyticsHeader.tsx
new file mode 100644
index 0000000..bbb6014
--- /dev/null
+++ b/frontend/app/components/analytics-comp/AnalyticsHeader.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import Link from "next/link";
+
+type AnalyticsHeaderProps = {
+ sprint: string;
+ sprints: string[];
+ query: string;
+ onQueryChange: (query: string) => void;
+ onSprintChange: (sprint: string) => void;
+};
+
+export default function AnalyticsHeader({
+ sprint,
+ sprints,
+ query,
+ onQueryChange,
+ onSprintChange,
+}: AnalyticsHeaderProps) {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/analytics-comp/AnalyticsMobileNav.tsx b/frontend/app/components/analytics-comp/AnalyticsMobileNav.tsx
new file mode 100644
index 0000000..7063548
--- /dev/null
+++ b/frontend/app/components/analytics-comp/AnalyticsMobileNav.tsx
@@ -0,0 +1,29 @@
+import Link from "next/link";
+
+export default function AnalyticsMobileNav() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/analytics-comp/AnalyticsSideBar.tsx b/frontend/app/components/analytics-comp/AnalyticsSideBar.tsx
new file mode 100644
index 0000000..9deb9c1
--- /dev/null
+++ b/frontend/app/components/analytics-comp/AnalyticsSideBar.tsx
@@ -0,0 +1,53 @@
+import Link from "next/link";
+
+export default function AnalyticsSidebar() {
+ return (
+
+ );
+}
diff --git a/frontend/app/components/analytics-comp/MemberAnalytics.tsx b/frontend/app/components/analytics-comp/MemberAnalytics.tsx
new file mode 100644
index 0000000..2084425
--- /dev/null
+++ b/frontend/app/components/analytics-comp/MemberAnalytics.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import type { MemberPerformance } from "./PerformanceSummary";
+
+type MemberAnalyticsProps = {
+ members: MemberPerformance[];
+ selectedMember: MemberPerformance;
+ onSelectMember: (memberId: string) => void;
+};
+
+export default function MemberAnalytics({
+ members,
+ selectedMember,
+ onSelectMember,
+}: MemberAnalyticsProps) {
+ const completionRate = Math.round((selectedMember.completed / selectedMember.assigned) * 100);
+ const reviewLoad = Math.min(100, selectedMember.reviews * 4);
+ const otherPercent = 100 - completionRate;
+
+ return (
+
+
+
+
+
+ {selectedMember.name.split(" ").map((part) => part[0]).join("")}
+
+
+
{selectedMember.name}
+
{selectedMember.role}
+
+
+
+
+ Focus: {selectedMember.focus}
+
+
+
+
+
+
+ Activity Frequency
+
+
+ {selectedMember.activity.map((height, index) => (
+
+ ))}
+
+
+
+
+
+ Workload Distribution
+
+
+
+
+
+
+ {completionRate}% Completed
+
+
+
+ {otherPercent}% Open
+
+
+
+ {reviewLoad}% Review Load
+
+
+
+
+
+
+
+
+
Members
+
+ {members.map((member) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/app/components/analytics-comp/PerformanceSummary.tsx b/frontend/app/components/analytics-comp/PerformanceSummary.tsx
new file mode 100644
index 0000000..48ef34a
--- /dev/null
+++ b/frontend/app/components/analytics-comp/PerformanceSummary.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+export type AnalyticsMetric = "completed" | "assigned";
+
+export type MemberPerformance = {
+ id: string;
+ name: string;
+ role: string;
+ assigned: number;
+ completed: number;
+ reviews: number;
+ focus: string;
+ activity: number[];
+ image: string;
+};
+
+type PerformanceSummaryProps = {
+ members: MemberPerformance[];
+ metric: AnalyticsMetric;
+ onMetricChange: (metric: AnalyticsMetric) => void;
+};
+
+export default function PerformanceSummary({
+ members,
+ metric,
+ onMetricChange,
+}: PerformanceSummaryProps) {
+ const completedTotal = members.reduce((sum, member) => sum + member.completed, 0);
+ const assignedTotal = members.reduce((sum, member) => sum + member.assigned, 0);
+ const reviewTotal = members.reduce((sum, member) => sum + member.reviews, 0);
+ const completionRate = Math.round((completedTotal / assignedTotal) * 100);
+
+ return (
+
+
+
+
+
+
+
+ Task Completion Velocity
+
+
+ Toggle between completed work and assigned workload.
+
+
+
+
+ {(["completed", "assigned"] as const).map((item) => (
+
+ ))}
+
+
+
+
+ {members.map((member) => {
+ const activeValue = member[metric];
+ return (
+
+
+
+ {member.name.split(" ")[0]}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/frontend/app/components/analytics-comp/SprintLeaderboard.tsx b/frontend/app/components/analytics-comp/SprintLeaderboard.tsx
new file mode 100644
index 0000000..772dcd8
--- /dev/null
+++ b/frontend/app/components/analytics-comp/SprintLeaderboard.tsx
@@ -0,0 +1,110 @@
+"use client";
+
+import { useMemo } from "react";
+import type { MemberPerformance } from "./PerformanceSummary";
+
+export type LeaderboardSortKey = "score" | "completed" | "reviews";
+
+type SprintLeaderboardProps = {
+ sprint: string;
+ members: MemberPerformance[];
+ sortKey: LeaderboardSortKey;
+ onSortChange: (sortKey: LeaderboardSortKey) => void;
+};
+
+function impactScore(member: MemberPerformance) {
+ return Number((member.completed * 0.8 + member.reviews * 1.6).toFixed(1));
+}
+
+export default function SprintLeaderboard({
+ sprint,
+ members,
+ sortKey,
+ onSortChange,
+}: SprintLeaderboardProps) {
+ const sortedRows = useMemo(() => {
+ return [...members].sort((a, b) => {
+ if (sortKey === "score") return impactScore(b) - impactScore(a);
+ return b[sortKey] - a[sortKey];
+ });
+ }, [members, sortKey]);
+
+ return (
+
+
+
+
+
+ Sprint Participation Leaderboard
+
+
+ Sort by impact score, completed work, or peer reviews.
+
+
+
+
+
+ Active Sprint: {sprint.replace("Sprint ", "")}
+
+ {(["score", "completed", "reviews"] as const).map((item) => (
+
+ ))}
+
+
+
+
+
+
+
+ | Rank |
+ Member |
+ Stories Resolved |
+ Peer Reviews |
+ Impact Score |
+
+
+
+ {sortedRows.map((row, index) => (
+
+ |
+ {(index + 1).toString().padStart(2, "0")}
+ |
+
+
+ 
+
+ {row.name}
+ {row.focus}
+
+
+ |
+ {row.completed} |
+ {row.reviews} |
+ {impactScore(row)} |
+
+ ))}
+ {sortedRows.length === 0 && (
+
+ |
+ No members match the current search.
+ |
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/kanban/KanbanBoard.tsx b/frontend/app/components/kanban/KanbanBoard.tsx
index 2d773ce..72b7fdd 100644
--- a/frontend/app/components/kanban/KanbanBoard.tsx
+++ b/frontend/app/components/kanban/KanbanBoard.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import {
DndContext,
@@ -11,6 +11,9 @@ import {
useSensors,
DragOverlay,
defaultDropAnimationSideEffects,
+ type DragEndEvent,
+ type DragOverEvent,
+ type DragStartEvent,
} from "@dnd-kit/core";
import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable";
import { KanbanColumn } from "./KanbanColumn";
@@ -30,12 +33,18 @@ const COLUMNS = [
{ id: "todo", title: "To Do" },
{ id: "in_progress", title: "In Progress" },
{ id: "done", title: "Done" },
-];
+] as const;
+
+type TaskStatus = Task["status"];
+
+function isTaskStatus(value: unknown): value is TaskStatus {
+ return COLUMNS.some((column) => column.id === value);
+}
export function KanbanBoard() {
const [tasks, setTasks] = useState([]);
const [activeTask, setActiveTask] = useState(null);
- const [socket, setSocket] = useState(null);
+ const socketRef = useRef | null>(null);
const handleEditTask = async (task: Task) => {
const newTitle = window.prompt("Edit title:", task.title);
@@ -63,13 +72,27 @@ export function KanbanBoard() {
}
};
+ const fetchTasks = async () => {
+ try {
+ const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000"}/api/tasks`);
+ if (res.ok) {
+ const data = await res.json();
+ setTasks(data);
+ }
+ } catch (error) {
+ console.error("Failed to fetch tasks", error);
+ }
+ };
+
useEffect(() => {
// Assuming backend runs on 5000 in dev
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
const newSocket = io(apiUrl);
- setSocket(newSocket);
+ socketRef.current = newSocket;
- fetchTasks();
+ const loadTimer = window.setTimeout(() => {
+ void fetchTasks();
+ }, 0);
newSocket.on("task-moved", (movedTask: Task) => {
setTasks((prev) => {
@@ -97,22 +120,12 @@ export function KanbanBoard() {
});
return () => {
+ window.clearTimeout(loadTimer);
newSocket.disconnect();
+ socketRef.current = null;
};
}, []);
- const fetchTasks = async () => {
- try {
- const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000"}/api/tasks`);
- if (res.ok) {
- const data = await res.json();
- setTasks(data);
- }
- } catch (error) {
- console.error("Failed to fetch tasks", error);
- }
- };
-
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
@@ -124,13 +137,13 @@ export function KanbanBoard() {
})
);
- const handleDragStart = (event: any) => {
+ const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const task = tasks.find((t) => t.id === active.id);
if (task) setActiveTask(task);
};
- const handleDragOver = (event: any) => {
+ const handleDragOver = (event: DragOverEvent) => {
const { active, over } = event;
if (!over) return;
@@ -166,13 +179,14 @@ export function KanbanBoard() {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
const newTasks = [...tasks];
- newTasks[activeIndex].status = overId as any;
+ if (activeIndex === -1 || !isTaskStatus(overId)) return tasks;
+ newTasks[activeIndex].status = overId;
return arrayMove(newTasks, activeIndex, activeIndex);
});
}
};
- const handleDragEnd = async (event: any) => {
+ const handleDragEnd = async (event: DragEndEvent) => {
setActiveTask(null);
const { active, over } = event;
@@ -187,7 +201,8 @@ export function KanbanBoard() {
let newStatus = activeTask.status;
if (over.data.current?.type === "Column") {
- newStatus = over.id as any;
+ if (!isTaskStatus(over.id)) return;
+ newStatus = over.id;
} else if (over.data.current?.type === "Task") {
const overTask = tasks.find((t) => t.id === overId);
if (overTask) {
@@ -246,9 +261,9 @@ export function KanbanBoard() {
);
// Emit socket updates
- if (socket) {
+ if (socketRef.current) {
reorderedTasks.forEach((task) => {
- socket.emit("task-moved", task);
+ socketRef.current?.emit("task-moved", task);
});
}
} catch (error) {
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx
index 95e8745..4fa2c88 100644
--- a/frontend/app/dashboard/page.tsx
+++ b/frontend/app/dashboard/page.tsx
@@ -1,15 +1,13 @@
"use client";
import { useState } from "react";
+import type React from "react";
import Link from "next/link";
import {
Search,
TrendingUp,
Rocket,
AlertTriangle,
- FolderKanban,
- Users,
- MessageSquare,
} from "lucide-react";
import {
LineChart,
@@ -138,7 +136,7 @@ export default function Dashboard() {
Productivity Analytics
- Track your team's performance trends over time
+ Track your team's performance trends over time
@@ -191,6 +189,32 @@ export default function Dashboard() {
Key analytics and intelligent recommendations to guide your
workflow.
+
+
+ Overview
+
+
+ Analytics
+
+
+ Feed
+
+
+ Tasks
+
+
@@ -207,7 +231,7 @@ export default function Dashboard() {
View detailed report →
@@ -223,9 +247,12 @@ export default function Dashboard() {
today can further boost efficiency by 10%.
-
+
{/* 🚧 PROJECTS IN PROGRESS */}
@@ -257,7 +284,7 @@ export default function Dashboard() {
Manage projects →
@@ -277,6 +304,7 @@ export default function Dashboard() {
{member.name}
@@ -323,7 +351,13 @@ const btnStrong =
"rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800 transition";
/* 🔹 Card */
-function Card({ icon, title, link }: any) {
+type CardProps = {
+ icon: React.ReactNode;
+ title: string;
+ link: string;
+};
+
+function Card({ icon, title, link }: CardProps) {
return (
{icon}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index f09a7ab..724a4e1 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -1,5 +1,19 @@
@import "tailwindcss";
+@theme {
+ --color-primary: #0f766e;
+ --color-primary-dark: #115e59;
+ --color-primary-light: #eef2e7;
+
+ --color-background: #f5f7f2;
+ --color-surface: #ffffff;
+
+ --color-text-main: #1d2a20;
+ --color-text-muted: #5d6d62;
+
+ --color-border-subtle: #dbe4d5;
+}
+
:root {
--bg-page: #f5f7f2;
--bg-panel: #ffffff;
diff --git a/frontend/app/hooks/useAuth.ts b/frontend/app/hooks/useAuth.ts
index bd20c7d..3588a12 100644
--- a/frontend/app/hooks/useAuth.ts
+++ b/frontend/app/hooks/useAuth.ts
@@ -3,9 +3,10 @@
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase";
import { useRouter } from "next/navigation";
+import type { User } from "@supabase/supabase-js";
export function useAuth() {
- const [user, setUser] = useState
(null);
+ const [user, setUser] = useState(null);
const router = useRouter();
useEffect(() => {
@@ -40,4 +41,4 @@ export function useAuth() {
}, [router]);
return user;
-}
\ No newline at end of file
+}
diff --git a/frontend/app/insights pages/page.tsx b/frontend/app/insights pages/page.tsx
new file mode 100644
index 0000000..8e093c9
--- /dev/null
+++ b/frontend/app/insights pages/page.tsx
@@ -0,0 +1,77 @@
+"use client"
+
+const InsightsPage = () => {
+ const navItems = [
+ {
+ label: 'Overview',
+ active: true,
+ },
+ {
+ label: "Analytics",
+ active: false,
+ },
+ {
+ label: "Tasks",
+ active: false,
+ },
+ {
+ label: "Feed",
+ active: false,
+ },
+ ];
+ return (
+
+
+
+
+
+
+
+
+ Project Alpha Evolution
+
+
+ Project
+
+ Evolution
+
+
+ Acomprehensive deep-dive into project development,
+ velocity trends, and realtime contributor synchronization.
+
+
+
+
+
+
+
+
+ )
+}
+
+export default InsightsPage
\ No newline at end of file
diff --git a/frontend/app/insights/analytics/page.tsx b/frontend/app/insights/analytics/page.tsx
new file mode 100644
index 0000000..15bda40
--- /dev/null
+++ b/frontend/app/insights/analytics/page.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import AnalyticsSidebar from "@/app/components/analytics-comp/AnalyticsSideBar";
+import AnalyticsHeader from "@/app/components/analytics-comp/AnalyticsHeader";
+import PerformanceSummary, {
+ type AnalyticsMetric,
+ type MemberPerformance,
+} from "@/app/components/analytics-comp/PerformanceSummary";
+import MemberAnalytics from "@/app/components/analytics-comp/MemberAnalytics";
+import SprintLeaderboard, {
+ type LeaderboardSortKey,
+} from "@/app/components/analytics-comp/SprintLeaderboard";
+import AnalyticsMobileNav from "@/app/components/analytics-comp/AnalyticsMobileNav";
+
+const sprintData: Record = {
+ "Sprint 42": [
+ { id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", assigned: 85, completed: 72, reviews: 18, focus: "Frontend", activity: [30, 50, 80, 100, 90, 60, 20, 85], image: "https://i.pravatar.cc/96?img=11" },
+ { id: "jordan", name: "Jordan Smith", role: "Backend Engineer", assigned: 60, completed: 58, reviews: 15, focus: "API", activity: [45, 65, 55, 80, 70, 64, 58, 75], image: "https://i.pravatar.cc/96?img=12" },
+ { id: "casey", name: "Casey Morgan", role: "Fullstack Developer", assigned: 95, completed: 90, reviews: 24, focus: "Fullstack", activity: [70, 40, 90, 100, 50, 80, 30, 95], image: "https://i.pravatar.cc/96?img=13" },
+ { id: "riley", name: "Riley Lee", role: "QA Analyst", assigned: 45, completed: 45, reviews: 11, focus: "QA", activity: [35, 55, 42, 62, 78, 45, 68, 72], image: "https://i.pravatar.cc/96?img=14" },
+ { id: "morgan", name: "Morgan Patel", role: "Product Engineer", assigned: 80, completed: 40, reviews: 9, focus: "Product", activity: [25, 35, 52, 48, 60, 42, 38, 50], image: "https://i.pravatar.cc/96?img=15" },
+ { id: "quinn", name: "Quinn Taylor", role: "DevOps Engineer", assigned: 70, completed: 68, reviews: 14, focus: "Ops", activity: [55, 68, 72, 75, 62, 74, 80, 78], image: "https://i.pravatar.cc/96?img=16" },
+ ],
+ "Sprint 41": [
+ { id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", assigned: 76, completed: 70, reviews: 21, focus: "Frontend", activity: [44, 58, 62, 88, 92, 70, 55, 81], image: "https://i.pravatar.cc/96?img=11" },
+ { id: "jordan", name: "Jordan Smith", role: "Backend Engineer", assigned: 72, completed: 61, reviews: 12, focus: "API", activity: [40, 48, 65, 72, 68, 71, 52, 64], image: "https://i.pravatar.cc/96?img=12" },
+ { id: "casey", name: "Casey Morgan", role: "Fullstack Developer", assigned: 88, completed: 84, reviews: 20, focus: "Fullstack", activity: [58, 74, 82, 95, 76, 86, 62, 91], image: "https://i.pravatar.cc/96?img=13" },
+ { id: "riley", name: "Riley Lee", role: "QA Analyst", assigned: 52, completed: 49, reviews: 16, focus: "QA", activity: [62, 60, 70, 66, 78, 74, 72, 80], image: "https://i.pravatar.cc/96?img=14" },
+ { id: "morgan", name: "Morgan Patel", role: "Product Engineer", assigned: 66, completed: 59, reviews: 10, focus: "Product", activity: [32, 46, 58, 62, 60, 55, 64, 68], image: "https://i.pravatar.cc/96?img=15" },
+ ],
+};
+
+type AnalyticsResponse = {
+ sprints: Array<{
+ label: string;
+ members: MemberPerformance[];
+ }>;
+};
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
+
+export default function InsightsAnalyticsPage() {
+ const [data, setData] = useState(sprintData);
+ const [sprint, setSprint] = useState("Sprint 42");
+ const [metric, setMetric] = useState("completed");
+ const [selectedMemberId, setSelectedMemberId] = useState("casey");
+ const [query, setQuery] = useState("");
+ const [sortKey, setSortKey] = useState("score");
+ const [isLoading, setIsLoading] = useState(false);
+ const [loadError, setLoadError] = useState("");
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function loadAnalytics() {
+ setIsLoading(true);
+ setLoadError("");
+ try {
+ const response = await fetch(`${API_URL}/api/analytics`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error("Failed to load analytics");
+ const body = (await response.json()) as AnalyticsResponse;
+ const nextData = body.sprints.reduce>(
+ (acc, item) => {
+ acc[item.label] = item.members;
+ return acc;
+ },
+ {}
+ );
+
+ if (Object.keys(nextData).length > 0) {
+ setData(nextData);
+ const firstSprint = Object.keys(nextData)[0];
+ setSprint((current) => (nextData[current] ? current : firstSprint));
+ setSelectedMemberId((current) => {
+ const activeMembers = nextData[firstSprint] || [];
+ return activeMembers.some((member) => member.id === current)
+ ? current
+ : activeMembers[0]?.id || current;
+ });
+ }
+ } catch (error) {
+ if ((error as Error).name !== "AbortError") {
+ setLoadError("Showing local analytics because the backend is unavailable.");
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ void loadAnalytics();
+ return () => controller.abort();
+ }, []);
+
+ const members = useMemo(
+ () => data[sprint] || Object.values(data)[0] || [],
+ [data, sprint]
+ );
+ const selectedMember = members.find((member) => member.id === selectedMemberId) ?? members[0];
+
+ const filteredMembers = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase();
+ if (!normalizedQuery) return members;
+ return members.filter((member) =>
+ [member.name, member.role, member.focus].some((value) =>
+ value.toLowerCase().includes(normalizedQuery)
+ )
+ );
+ }, [members, query]);
+
+ return (
+ <>
+ {
+ setSprint(nextSprint);
+ setSelectedMemberId(data[nextSprint][0]?.id || "");
+ }}
+ />
+
+
+
+ {(isLoading || loadError) && (
+
+ {isLoading ? "Refreshing analytics from backend..." : loadError}
+
+ )}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/app/insights/feed/page.tsx b/frontend/app/insights/feed/page.tsx
new file mode 100644
index 0000000..1a16508
--- /dev/null
+++ b/frontend/app/insights/feed/page.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import FeedSidebar from "@/app/components/Feed-comp/FeedSidebar";
+import FeedHeader, { type FeedFilter } from "@/app/components/Feed-comp/FeedHeader";
+import FeedList, { type FeedActivityItem } from "@/app/components/Feed-comp/FeedList";
+import FeedMobileNav from "@/app/components/Feed-comp/FeedMobileNav";
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
+
+const fallbackItems: FeedActivityItem[] = [
+ {
+ id: "fallback-deploy",
+ type: "code",
+ actor: "Alex Rivera",
+ action: "deployed to production",
+ title: "Production Environment: v2.4.0-rc1",
+ body: "Successfully deployed 14 services and updated the global edge configuration.",
+ time: "2 hours ago",
+ group: "Today",
+ meta: "Deployment",
+ image: "https://i.pravatar.cc/96?img=21",
+ progress: null,
+ },
+ {
+ id: "fallback-comment",
+ type: "discussion",
+ actor: "James Wilson",
+ action: "commented on a task",
+ title: "Heatmap spacing",
+ body: "The insights heatmap needs more padding around the legend on compact displays.",
+ time: "Yesterday",
+ group: "Yesterday",
+ meta: "Discussion",
+ image: "https://i.pravatar.cc/96?img=22",
+ progress: null,
+ },
+];
+
+export default function InsightsFeedPage() {
+ const [items, setItems] = useState(fallbackItems);
+ const [filter, setFilter] = useState("All");
+ const [query, setQuery] = useState("");
+ const [visibleCount, setVisibleCount] = useState(6);
+ const [isLoading, setIsLoading] = useState(false);
+ const [status, setStatus] = useState("");
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function loadFeed() {
+ setIsLoading(true);
+ setStatus("");
+ try {
+ const response = await fetch(`${API_URL}/api/feed`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error("Failed to load feed");
+ const body = (await response.json()) as { items: FeedActivityItem[] };
+ setItems(body.items.length > 0 ? body.items : fallbackItems);
+ } catch (error) {
+ if ((error as Error).name !== "AbortError") {
+ setStatus("Showing local activity because the backend feed is unavailable.");
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ void loadFeed();
+ return () => controller.abort();
+ }, []);
+
+ const filteredItems = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase();
+ return items.filter((item) => {
+ const matchesFilter =
+ filter === "All" ||
+ (filter === "Code" && item.type === "code") ||
+ (filter === "Discussion" && item.type === "discussion") ||
+ (filter === "Milestones" && item.type === "milestone");
+ const matchesQuery =
+ !normalizedQuery ||
+ [item.actor, item.action, item.title, item.body, item.meta].some((value) =>
+ String(value || "").toLowerCase().includes(normalizedQuery)
+ );
+ return matchesFilter && matchesQuery;
+ });
+ }, [filter, items, query]);
+
+ const createInsight = async () => {
+ const title = window.prompt("Insight title");
+ if (!title?.trim()) return;
+ const body = window.prompt("Insight details");
+ if (!body?.trim()) return;
+
+ try {
+ const response = await fetch(`${API_URL}/api/feed`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ title: title.trim(), body: body.trim(), type: "discussion" }),
+ });
+ if (!response.ok) throw new Error("Failed to create insight");
+ const result = (await response.json()) as { item: FeedActivityItem };
+ setItems((current) => [result.item, ...current]);
+ setStatus("Insight added to the backend feed.");
+ } catch {
+ const localItem: FeedActivityItem = {
+ id: `local-${Date.now()}`,
+ type: "discussion",
+ actor: "You",
+ action: "created a local insight",
+ title: title.trim(),
+ body: body.trim(),
+ time: "Just now",
+ group: "Today",
+ meta: "Local draft",
+ image: null,
+ progress: null,
+ };
+ setItems((current) => [localItem, ...current]);
+ setStatus("Backend was unavailable, so this insight was added locally.");
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ setVisibleCount((count) => count + 4)}
+ />
+
+
+
+ >
+ );
+}
diff --git a/frontend/app/insights/layout.tsx b/frontend/app/insights/layout.tsx
new file mode 100644
index 0000000..9ee39b6
--- /dev/null
+++ b/frontend/app/insights/layout.tsx
@@ -0,0 +1,16 @@
+// app/insights/layout.tsx
+import React from "react";
+import InsightsSidebar from "@/app/components/InsightsSidebar";
+
+export default function InsightsLayout({
+ children,
+}: { children: React.ReactNode }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/insights/overview/page.tsx b/frontend/app/insights/overview/page.tsx
new file mode 100644
index 0000000..77d6ac5
--- /dev/null
+++ b/frontend/app/insights/overview/page.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import TopHeader from "@/app/components/Tasks-comp/TopHeader";
+import OverviewMain from "@/app/components/Overview-comp/OverviewMain";
+
+export default function InsightsOverviewPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/insights/tasks/page.tsx b/frontend/app/insights/tasks/page.tsx
new file mode 100644
index 0000000..e4107db
--- /dev/null
+++ b/frontend/app/insights/tasks/page.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import TasksHeader from "@/app/components/Tasks-comp/TasksHeader";
+import TasksSidebar from "@/app/components/Tasks-comp/TasksSideBar";
+import TaskPulseBar, { type TaskPulseStage } from "@/app/components/Tasks-comp/TasksPulsebar";
+import TaskFlow, { type TaskFlowNode } from "@/app/components/Tasks-comp/TasksFlow";
+import TaskHistoryTable, { type TaskHistoryRow } from "@/app/components/Tasks-comp/TaskHistoryTable";
+import MobileNav from "@/app/components/Tasks-comp/MobileNav";
+
+type TaskInsights = {
+ summary: {
+ totalTasks: number;
+ completedTasks: number;
+ activeTasks: number;
+ completionRate: number;
+ };
+ stages: TaskPulseStage[];
+ flowNodes: TaskFlowNode[];
+ history: TaskHistoryRow[];
+};
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
+
+const fallbackInsights: TaskInsights = {
+ summary: {
+ totalTasks: 0,
+ completedTasks: 0,
+ activeTasks: 0,
+ completionRate: 0,
+ },
+ stages: [
+ { label: "Todo", value: 0, active: false },
+ { label: "In Progress", value: 0, active: false },
+ { label: "Review", value: 0, active: false },
+ { label: "Total Cycle", value: 0, active: false },
+ ],
+ flowNodes: [
+ { label: "Todo", sub: "Queue", value: 0, active: false },
+ { label: "In Progress", sub: "Active", value: 0, active: false },
+ { label: "Review", sub: "Verify", value: 0, active: false },
+ { label: "Done", sub: "Archived", value: 0, active: false },
+ ],
+ history: [],
+};
+
+export default function InsightsTasksPage() {
+ const [insights, setInsights] = useState(fallbackInsights);
+ const [query, setQuery] = useState("");
+ const [status, setStatus] = useState("Loading backend task insights...");
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function loadTaskInsights() {
+ try {
+ const response = await fetch(`${API_URL}/api/insights/tasks`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error("Failed to load task insights");
+ const body = (await response.json()) as TaskInsights;
+ setInsights(body);
+ setStatus("Synced with backend task data.");
+ } catch (error) {
+ if ((error as Error).name !== "AbortError") {
+ setStatus("Showing local task insights because backend data is unavailable.");
+ }
+ }
+ }
+
+ void loadTaskInsights();
+ return () => controller.abort();
+ }, []);
+
+ const filteredHistory = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase();
+ if (!normalizedQuery) return insights.history;
+ return insights.history.filter((row) =>
+ [row.task, row.assignee, row.state, row.trigger].some((value) =>
+ value.toLowerCase().includes(normalizedQuery)
+ )
+ );
+ }, [insights.history, query]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Task Journey Tracking
+
+
+ Backend-backed lifecycle analysis for {insights.summary.totalTasks} tasks, {insights.summary.completionRate}% complete.
+
+
{status}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/app/lib/navItems.ts b/frontend/app/lib/navItems.ts
new file mode 100644
index 0000000..428bdd8
--- /dev/null
+++ b/frontend/app/lib/navItems.ts
@@ -0,0 +1,21 @@
+export type NavItems = {
+ label: string;
+ href: string;
+};
+
+export const navItems: NavItems[] = [
+ {
+ label: "Overview", href: "/insights/overview"
+ },
+ {
+ label: "Analytics", href: "/insights/analytics"
+ },
+ {
+ label: "Feed", href: "/insights/feed"
+ },
+ {
+ label: "Tasks", href: "/insights/tasks"
+ },
+
+];
+
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
index daf3749..2b9ab14 100644
--- a/frontend/app/login/page.tsx
+++ b/frontend/app/login/page.tsx
@@ -16,7 +16,7 @@ export default function Login() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
- setNextPath(params.get("next"));
+ queueMicrotask(() => setNextPath(params.get("next")));
}, []);
const handleLogin = async () => {
@@ -97,4 +97,4 @@ export default function Login() {
);
-}
\ No newline at end of file
+}
diff --git a/frontend/app/projects/page.tsx b/frontend/app/projects/page.tsx
index 2f048eb..eb10c81 100644
--- a/frontend/app/projects/page.tsx
+++ b/frontend/app/projects/page.tsx
@@ -6,12 +6,22 @@ import { isSupabaseConfigured, supabase } from "@/app/lib/supabase";
import { useState, useEffect } from "react";
import ProjectDialog from "@/app/components/ProjectDialog";
+type Project = {
+ id: string;
+ name: string;
+ desc: string;
+ members: number;
+ due: string | null;
+ tags: string[];
+ createdAt: string;
+};
+
export default function ProjectsPage() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [open, setOpen] = useState(false);
- const [projects, setProjects] = useState([]);
+ const [projects, setProjects] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Debounce search term
@@ -86,7 +96,7 @@ export default function ProjectsPage() {
setOpen(true);
};
- const handleProjectCreated = (newProject: any) => {
+ const handleProjectCreated = (newProject: Project) => {
setProjects((prev) => [
{
name: newProject.name || "",
@@ -197,4 +207,4 @@ export default function ProjectsPage() {
)}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx
index d5915a3..c6b7616 100644
--- a/frontend/app/signup/page.tsx
+++ b/frontend/app/signup/page.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
-import { isSupabaseConfigured, supabase } from "@/app/lib/supabase";
+import { supabase } from "@/app/lib/supabase";
function generatePassword(length = 14): string {
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -48,7 +48,7 @@ export default function Signup() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
- setNextPath(params.get("next"));
+ queueMicrotask(() => setNextPath(params.get("next")));
}, []);
const handleGeneratePassword = () => {
@@ -107,14 +107,14 @@ export default function Signup() {
mobile: mobile.trim(),
}),
});
- let result: any = {};
- let error: any = null;
+ let result: { error?: string } = {};
+ let error: { message: string } | null = null;
try {
result = await res.json();
if (result.error){
error = {message: result.error}
}
- } catch (e) {
+ } catch {
error = {message: "Failed to parse server response."}
setErrorMsg("An unexpected error occurred. Please try again.");
setLoading(false);
@@ -271,4 +271,4 @@ export default function Signup() {
);
-}
\ No newline at end of file
+}