diff --git a/app/models/user.ts b/app/models/user.ts index f44b85f..13d302e 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,4 +1,4 @@ -import { eq, inArray, and, or, ilike, sql } from "drizzle-orm"; +import { eq, inArray, and, or, ilike, isNull, sql } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { generateFlagConfig } from "~/lib/flag-generator"; @@ -113,17 +113,21 @@ export async function findUsersPaginated(options: { search?: string; }): Promise<{ users: User[]; total: number; hasMore: boolean }> { const db = database(); - const searchFilter = options.search - ? or( - ilike(schema.users.email, `%${options.search}%`), - ilike(schema.users.username, `%${options.search}%`), - ilike(schema.users.displayName, `%${options.search}%`), + const notDeleted = isNull(schema.users.deletedAt); + const filter = options.search + ? and( + notDeleted, + or( + ilike(schema.users.email, `%${options.search}%`), + ilike(schema.users.username, `%${options.search}%`), + ilike(schema.users.displayName, `%${options.search}%`), + ), ) - : undefined; + : notDeleted; const [users, [{ count }]] = await Promise.all([ db.query.users.findMany({ - where: searchFilter, + where: filter, orderBy: (users, { asc }) => [asc(users.displayName)], limit: options.limit, offset: options.offset, @@ -131,7 +135,7 @@ export async function findUsersPaginated(options: { db .select({ count: sql`count(*)::int` }) .from(schema.users) - .where(searchFilter), + .where(filter), ]); return { users, total: count, hasMore: options.offset + users.length < count }; } diff --git a/app/root.tsx b/app/root.tsx index 7fd76c0..d9dc04d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -23,7 +23,7 @@ import logoUrl from "../public/logo.svg?url"; // Paths that should never trigger the onboarding redirect. // Trailing-slash entries are matched as prefixes; others require exact match or sub-path. -const ONBOARDING_EXEMPT = ["/onboarding", "/api/", "/login", "/register", "/check-email", "/forgot-password", "/reset-password"]; +const ONBOARDING_EXEMPT = ["/onboarding", "/api/", "/admin", "/login", "/register", "/check-email", "/forgot-password", "/reset-password"]; export function isOnboardingExempt(pathname: string): boolean { return ONBOARDING_EXEMPT.some((p) => diff --git a/app/routes/__tests__/root.onboarding-exempt.test.ts b/app/routes/__tests__/root.onboarding-exempt.test.ts index b0768a8..8df54f4 100644 --- a/app/routes/__tests__/root.onboarding-exempt.test.ts +++ b/app/routes/__tests__/root.onboarding-exempt.test.ts @@ -33,6 +33,12 @@ describe("isOnboardingExempt", () => { expect(isOnboardingExempt("/loginpage")).toBe(false); }); + it("exempts /admin and all sub-paths", () => { + expect(isOnboardingExempt("/admin")).toBe(true); + expect(isOnboardingExempt("/admin/users")).toBe(true); + expect(isOnboardingExempt("/admin/sports-seasons/123")).toBe(true); + }); + it("does not exempt normal app routes", () => { expect(isOnboardingExempt("/")).toBe(false); expect(isOnboardingExempt("/leagues/abc")).toBe(false); diff --git a/app/routes/admin.users.tsx b/app/routes/admin.users.tsx index 72b67fd..c8c5717 100644 --- a/app/routes/admin.users.tsx +++ b/app/routes/admin.users.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Form, redirect, useSearchParams } from "react-router"; import type { Route } from "./+types/admin.users"; @@ -48,14 +48,14 @@ export async function loader(args: Route.LoaderArgs) { const page = Math.max(0, parseInt(searchParams.get("page") ?? "0", 10) || 0); const search = searchParams.get("search") ?? ""; - const { users, total, hasMore } = await findUsersPaginated({ + const { users, total } = await findUsersPaginated({ limit: PAGE_SIZE, offset: page * PAGE_SIZE, search: search || undefined, }); const totalPages = Math.ceil(total / PAGE_SIZE); - return { users, page, totalPages, total, hasMore, search }; + return { users, page, totalPages, total, search }; } export async function action(args: Route.ActionArgs) { @@ -107,6 +107,13 @@ export default function AdminUsers({ loaderData, actionData }: Route.ComponentPr const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(""); + useEffect(() => { + if (actionData && "success" in actionData && actionData.success) { + setEditingId(null); + setEditValue(""); + } + }, [actionData]); + function startEdit(userId: string, currentUsername: string | null) { setEditingId(userId); setEditValue(currentUsername ?? ""); @@ -125,11 +132,9 @@ export default function AdminUsers({ loaderData, actionData }: Route.ComponentPr return (
-
-
-

Users

-

{total} total users

-
+
+

Users

+

{total} total users

@@ -199,7 +204,6 @@ export default function AdminUsers({ loaderData, actionData }: Route.ComponentPr variant="ghost" className="h-7 w-7" title="Save" - onClick={() => setEditingId(null)} > diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx index 35c600d..4824c02 100644 --- a/app/routes/settings.tsx +++ b/app/routes/settings.tsx @@ -123,17 +123,21 @@ export async function action(args: Route.ActionArgs): Promise