Compare commits
2 Commits
feature/ca
...
2a00b2d31f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a00b2d31f | |||
| 6e3ce4a31f |
@@ -162,12 +162,7 @@ Plans:
|
||||
### Phase 999.1: Rewrite E2E Tests for OIDC Auth (BACKLOG)
|
||||
**Goal**: E2E tests currently expect local username/password login but auth moved to external OIDC (Logto). Rewrite with mock OIDC provider or API-key-based auth bypass. Seed migration to Postgres is already done.
|
||||
**Requirements**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 26-01-PLAN.md — Discovery service layer with cursor pagination (TDD)
|
||||
- [x] 26-02-PLAN.md — Discovery routes, server registration, and client hooks
|
||||
- [ ] 26-03-PLAN.md — Landing page UI and PublicSetupCard enhancement
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
@@ -175,12 +170,7 @@ Plans:
|
||||
### Phase 999.2: Revamp Onboarding Flow (BACKLOG)
|
||||
**Goal**: Redesign the onboarding experience to match the current app style and flow. Replace the manual item edit form with the catalog search function. Visual refresh to align with the newer UI patterns.
|
||||
**Requirements**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 26-01-PLAN.md — Discovery service layer with cursor pagination (TDD)
|
||||
- [ ] 26-02-PLAN.md — Discovery routes, server registration, and client hooks
|
||||
- [ ] 26-03-PLAN.md — Landing page UI and PublicSetupCard enhancement
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
@@ -188,12 +178,15 @@ Plans:
|
||||
### Phase 999.3: Public Access Auth Model (BACKLOG)
|
||||
**Goal**: Rework auth so the app is accessible without logging in. Currently all routes require authentication, but public-facing pages (discovery/browse, shared setups, public profiles) should be viewable by unauthenticated users. Auth only required for write operations and personal data.
|
||||
**Requirements**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 26-01-PLAN.md — Discovery service layer with cursor pagination (TDD)
|
||||
- [ ] 26-02-PLAN.md — Discovery routes, server registration, and client hooks
|
||||
- [ ] 26-03-PLAN.md — Landing page UI and PublicSetupCard enhancement
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
|
||||
### Phase 999.4: Top Nav Navigation Restructure & Search Bar Rethink (BACKLOG)
|
||||
**Goal**: Replace dashboard-based navigation with a persistent top nav bar (Home, Collection, future sections). Collection consolidates gear, threads, and setups under one section. Rethink the catalog search overlay appearance and interaction when entering from collection context.
|
||||
**Requirements**: TBD
|
||||
**Plans**: TBD
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
|
||||
@@ -143,12 +143,14 @@ function TrendingCategoriesSection() {
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => `cat-skeleton-${i}`).map(
|
||||
(key) => (
|
||||
<div
|
||||
key={i}
|
||||
key={key}
|
||||
className="h-8 w-24 bg-gray-100 rounded-full animate-pulse"
|
||||
/>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -172,9 +174,9 @@ function SectionSkeleton({ count, aspect }: { count: number; aspect: string }) {
|
||||
<div
|
||||
className={`grid ${aspect === "none" ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"} gap-4`}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
{Array.from({ length: count }, (_, i) => `skeleton-${i}`).map((key) => (
|
||||
<div
|
||||
key={i}
|
||||
key={key}
|
||||
className="bg-white rounded-xl border border-gray-100 overflow-hidden animate-pulse"
|
||||
>
|
||||
{aspect !== "none" && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { and, count, desc, eq, isNotNull, lt, sql } from "drizzle-orm";
|
||||
import { db as prodDb } from "../../db/index.ts";
|
||||
import { globalItems, setups, setupItems, users } from "../../db/schema.ts";
|
||||
import { globalItems, setupItems, setups, users } from "../../db/schema.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
|
||||
@@ -44,10 +44,7 @@ export async function getPopularSetups(
|
||||
.leftJoin(users, eq(users.id, setups.userId))
|
||||
.where(eq(setups.isPublic, true))
|
||||
.groupBy(setups.id, setups.name, setups.createdAt, users.displayName)
|
||||
.orderBy(
|
||||
desc(sql<number>`COUNT(${setupItems.id})`),
|
||||
desc(setups.id),
|
||||
)
|
||||
.orderBy(desc(sql<number>`COUNT(${setupItems.id})`), desc(setups.id))
|
||||
.limit(fetchLimit);
|
||||
|
||||
// Apply cursor filter in JS (composite cursor: itemCount_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { globalItems, items, setups, setupItems } from "../../src/db/schema.ts";
|
||||
import { globalItems, setups } from "../../src/db/schema.ts";
|
||||
import { discoveryRoutes } from "../../src/server/routes/discovery.ts";
|
||||
import { createTestDb } from "../helpers/db.ts";
|
||||
|
||||
@@ -45,26 +45,6 @@ async function insertPublicSetup(
|
||||
return row;
|
||||
}
|
||||
|
||||
async function insertItem(
|
||||
db: TestDb["db"],
|
||||
userId: number,
|
||||
name: string,
|
||||
): Promise<number> {
|
||||
const [row] = await db
|
||||
.insert(items)
|
||||
.values({ name, categoryId: 1, userId })
|
||||
.returning();
|
||||
return row.id;
|
||||
}
|
||||
|
||||
async function addItemToSetup(
|
||||
db: TestDb["db"],
|
||||
setupId: number,
|
||||
itemId: number,
|
||||
) {
|
||||
await db.insert(setupItems).values({ setupId, itemId });
|
||||
}
|
||||
|
||||
describe("Discovery Routes", () => {
|
||||
let app: Hono;
|
||||
let db: TestDb["db"];
|
||||
|
||||
@@ -3,8 +3,8 @@ import { eq } from "drizzle-orm";
|
||||
import {
|
||||
globalItems,
|
||||
items,
|
||||
setups,
|
||||
setupItems,
|
||||
setups,
|
||||
users,
|
||||
} from "../../src/db/schema.ts";
|
||||
import {
|
||||
@@ -31,11 +31,7 @@ async function insertGlobalItem(
|
||||
return row;
|
||||
}
|
||||
|
||||
async function insertItem(
|
||||
db: TestDb["db"],
|
||||
userId: number,
|
||||
categoryId = 1,
|
||||
) {
|
||||
async function insertItem(db: TestDb["db"], userId: number, categoryId = 1) {
|
||||
const [row] = await db
|
||||
.insert(items)
|
||||
.values({ name: "Test Item", categoryId, userId })
|
||||
@@ -115,7 +111,11 @@ describe("Discovery Service", () => {
|
||||
const item2 = await insertItem(db, userId);
|
||||
const item3 = await insertItem(db, userId);
|
||||
|
||||
await insertPublicSetup(db, userId, "Setup A", [item1.id, item2.id, item3.id]);
|
||||
await insertPublicSetup(db, userId, "Setup A", [
|
||||
item1.id,
|
||||
item2.id,
|
||||
item3.id,
|
||||
]);
|
||||
await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
|
||||
await insertPublicSetup(db, userId, "Setup C", [item1.id]);
|
||||
|
||||
@@ -129,7 +129,11 @@ describe("Discovery Service", () => {
|
||||
const item2 = await insertItem(db, userId);
|
||||
const item3 = await insertItem(db, userId);
|
||||
|
||||
await insertPublicSetup(db, userId, "Setup A", [item1.id, item2.id, item3.id]);
|
||||
await insertPublicSetup(db, userId, "Setup A", [
|
||||
item1.id,
|
||||
item2.id,
|
||||
item3.id,
|
||||
]);
|
||||
await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
|
||||
await insertPublicSetup(db, userId, "Setup C", [item1.id]);
|
||||
|
||||
@@ -144,7 +148,8 @@ describe("Discovery Service", () => {
|
||||
|
||||
it("includes creatorName from users.displayName", async () => {
|
||||
// Update user display name
|
||||
await db.update(users)
|
||||
await db
|
||||
.update(users)
|
||||
.set({ displayName: "Jean-Luc" })
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
@@ -160,11 +165,20 @@ describe("Discovery Service", () => {
|
||||
describe("getRecentGlobalItems", () => {
|
||||
it("returns items ordered by createdAt descending", async () => {
|
||||
// Insert items with slight delay to get different timestamps
|
||||
const item1 = await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
|
||||
const item1 = await insertGlobalItem(db, {
|
||||
brand: "BrandA",
|
||||
model: "Model1",
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const item2 = await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
|
||||
const item2 = await insertGlobalItem(db, {
|
||||
brand: "BrandB",
|
||||
model: "Model2",
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const item3 = await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });
|
||||
const item3 = await insertGlobalItem(db, {
|
||||
brand: "BrandC",
|
||||
model: "Model3",
|
||||
});
|
||||
|
||||
const result = await getRecentGlobalItems(db);
|
||||
expect(result.items).toHaveLength(3);
|
||||
@@ -208,12 +222,36 @@ describe("Discovery Service", () => {
|
||||
describe("getTrendingCategories", () => {
|
||||
it("returns categories ordered by item count descending", async () => {
|
||||
// 3 items in Tents, 1 in Bags, 2 in Stoves
|
||||
await insertGlobalItem(db, { brand: "BrandA", model: "Tent1", category: "Tents" });
|
||||
await insertGlobalItem(db, { brand: "BrandB", model: "Tent2", category: "Tents" });
|
||||
await insertGlobalItem(db, { brand: "BrandC", model: "Tent3", category: "Tents" });
|
||||
await insertGlobalItem(db, { brand: "BrandD", model: "Bag1", category: "Bags" });
|
||||
await insertGlobalItem(db, { brand: "BrandE", model: "Stove1", category: "Stoves" });
|
||||
await insertGlobalItem(db, { brand: "BrandF", model: "Stove2", category: "Stoves" });
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandA",
|
||||
model: "Tent1",
|
||||
category: "Tents",
|
||||
});
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandB",
|
||||
model: "Tent2",
|
||||
category: "Tents",
|
||||
});
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandC",
|
||||
model: "Tent3",
|
||||
category: "Tents",
|
||||
});
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandD",
|
||||
model: "Bag1",
|
||||
category: "Bags",
|
||||
});
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandE",
|
||||
model: "Stove1",
|
||||
category: "Stoves",
|
||||
});
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandF",
|
||||
model: "Stove2",
|
||||
category: "Stoves",
|
||||
});
|
||||
|
||||
const result = await getTrendingCategories(db);
|
||||
expect(result).toHaveLength(3);
|
||||
@@ -226,7 +264,11 @@ describe("Discovery Service", () => {
|
||||
});
|
||||
|
||||
it("excludes items with null category", async () => {
|
||||
await insertGlobalItem(db, { brand: "BrandA", model: "Tent1", category: "Tents" });
|
||||
await insertGlobalItem(db, {
|
||||
brand: "BrandA",
|
||||
model: "Tent1",
|
||||
category: "Tents",
|
||||
});
|
||||
// No category — should be excluded
|
||||
await insertGlobalItem(db, { brand: "BrandB", model: "NoCategory" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user