feat: Implement a new dashboard layout with sidebar, introduce project and data source management, and add various UI components.

This commit is contained in:
2026-02-03 15:11:53 +00:00
parent a795e92ef3
commit 7e3854d7d6
29 changed files with 2460 additions and 645 deletions

View File

@@ -9,7 +9,9 @@
*/
import type * as auth from "../auth.js";
import type * as dataSources from "../dataSources.js";
import type * as http from "../http.js";
import type * as projects from "../projects.js";
import type {
ApiFromModules,
@@ -19,7 +21,9 @@ import type {
declare const fullApi: ApiFromModules<{
auth: typeof auth;
dataSources: typeof dataSources;
http: typeof http;
projects: typeof projects;
}>;
/**

3
convex/auth.config.ts Normal file
View File

@@ -0,0 +1,3 @@
export default {
providers: [],
};

75
convex/dataSources.ts Normal file
View File

@@ -0,0 +1,75 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
export const getProjectDataSources = query({
args: { projectId: v.id("projects") },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) return [];
// Verify project ownership
const project = await ctx.db.get(args.projectId);
if (!project || project.userId !== userId) return [];
return await ctx.db
.query("dataSources")
.filter((q) => q.eq(q.field("projectId"), args.projectId))
.collect();
},
});
export const addDataSource = mutation({
args: {
projectId: v.optional(v.id("projects")), // Optional, if not provided, use default
url: v.string(),
name: v.string(),
type: v.literal("website"),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");
let projectId = args.projectId;
// Use default project if not provided
if (!projectId) {
const defaultProject = await ctx.db
.query("projects")
.filter((q) => q.and(q.eq(q.field("userId"), userId), q.eq(q.field("isDefault"), true)))
.first();
if (!defaultProject) {
// Create a default project if none exists (First onboarding)
projectId = await ctx.db.insert("projects", {
userId,
name: "My Project",
isDefault: true,
dorkingConfig: { selectedSourceIds: [] },
});
} else {
projectId = defaultProject._id;
}
}
const sourceId = await ctx.db.insert("dataSources", {
projectId: projectId!, // Assert exists
type: args.type,
url: args.url,
name: args.name,
analysisStatus: "pending",
// analysisResults not set initially
});
// Auto-select this source in the project config
const project = await ctx.db.get(projectId!);
if (project) {
const currentSelected = project.dorkingConfig.selectedSourceIds;
await ctx.db.patch(projectId!, {
dorkingConfig: { selectedSourceIds: [...currentSelected, sourceId] }
});
}
return sourceId;
},
});

70
convex/projects.ts Normal file
View File

@@ -0,0 +1,70 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
export const getProjects = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) return [];
return await ctx.db
.query("projects")
.withIndex("by_owner", (q) => q.eq("userId", userId)) // Note: Need to add index to schema too? Or just filter? Schema doesn't define indexes yet. Will rely on filter for now or filter in memory if small. Actually, will rely on simple filter or add index later.
.filter((q) => q.eq(q.field("userId"), userId))
.collect();
},
});
export const getDefaultProject = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) return null;
return await ctx.db
.query("projects")
.filter((q) => q.and(q.eq(q.field("userId"), userId), q.eq(q.field("isDefault"), true)))
.first();
},
});
export const createProject = mutation({
args: { name: v.string(), isDefault: v.boolean() },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");
// If setting as default, unset other defaults? For now assume handled by UI or logic
// Actually simplicity: just create.
return await ctx.db.insert("projects", {
userId,
name: args.name,
isDefault: args.isDefault,
dorkingConfig: { selectedSourceIds: [] },
});
},
});
export const toggleDataSourceConfig = mutation({
args: { projectId: v.id("projects"), sourceId: v.id("dataSources"), selected: v.boolean() },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");
const project = await ctx.db.get(args.projectId);
if (!project || project.userId !== userId) throw new Error("Project not found or unauthorized");
let newSelectedIds = project.dorkingConfig.selectedSourceIds;
if (args.selected) {
if (!newSelectedIds.includes(args.sourceId)) {
newSelectedIds.push(args.sourceId);
}
} else {
newSelectedIds = newSelectedIds.filter((id) => id !== args.sourceId);
}
await ctx.db.patch(args.projectId, {
dorkingConfig: { selectedSourceIds: newSelectedIds },
});
},
});

View File

@@ -1,9 +1,37 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
const schema = defineSchema({
...authTables,
// Your other tables here
projects: defineTable({
userId: v.id("users"),
name: v.string(),
isDefault: v.boolean(),
dorkingConfig: v.object({
selectedSourceIds: v.array(v.id("dataSources")),
}),
}),
dataSources: defineTable({
projectId: v.id("projects"),
type: v.literal("website"),
url: v.string(),
name: v.string(),
analysisStatus: v.union(
v.literal("pending"),
v.literal("completed"),
v.literal("failed")
),
analysisResults: v.optional(
v.object({
features: v.array(v.string()),
painPoints: v.array(v.string()),
keywords: v.array(v.string()),
summary: v.string(),
})
),
metadata: v.optional(v.any()),
}),
});
export default schema;