chore: fix lint errors — auto-format, isNaN, unused imports, button type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,8 @@
|
|||||||
"noLabelWithoutControl": "off"
|
"noLabelWithoutControl": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noExplicitAny": "off"
|
"noExplicitAny": "off",
|
||||||
|
"noArrayIndexKey": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noNonNullAssertion": "off"
|
"noNonNullAssertion": "off"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const args = Object.fromEntries(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tier = args["tier"] ? Number(args["tier"]) : 1;
|
const tier = args.tier ? Number(args.tier) : 1;
|
||||||
const dryRun = args["dry-run"] === "true";
|
const dryRun = args["dry-run"] === "true";
|
||||||
|
|
||||||
async function listActiveManufacturers(targetTier: number) {
|
async function listActiveManufacturers(targetTier: number) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const args = Object.fromEntries(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const manufacturerSlug = args["manufacturer"];
|
const manufacturerSlug = args.manufacturer;
|
||||||
const dryRun = args["dry-run"] === "true";
|
const dryRun = args["dry-run"] === "true";
|
||||||
|
|
||||||
if (!manufacturerSlug) {
|
if (!manufacturerSlug) {
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ const [user] = await db
|
|||||||
.update(users)
|
.update(users)
|
||||||
.set({ isAdmin: !revoke })
|
.set({ isAdmin: !revoke })
|
||||||
.where(eq(users.logtoSub, sub))
|
.where(eq(users.logtoSub, sub))
|
||||||
.returning({ id: users.id, logtoSub: users.logtoSub, isAdmin: users.isAdmin });
|
.returning({
|
||||||
|
id: users.id,
|
||||||
|
logtoSub: users.logtoSub,
|
||||||
|
isAdmin: users.isAdmin,
|
||||||
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.error(`User not found with logto_sub: ${sub}`);
|
console.error(`User not found with logto_sub: ${sub}`);
|
||||||
@@ -30,4 +34,6 @@ if (!user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const action = revoke ? "Revoked admin from" : "Granted admin to";
|
const action = revoke ? "Revoked admin from" : "Granted admin to";
|
||||||
console.log(`${action} user ${user.id} (${user.logtoSub}) — isAdmin: ${user.isAdmin}`);
|
console.log(
|
||||||
|
`${action} user ${user.id} (${user.logtoSub}) — isAdmin: ${user.isAdmin}`,
|
||||||
|
);
|
||||||
|
|||||||
@@ -244,7 +244,6 @@ export function ComparisonTable({
|
|||||||
return (
|
return (
|
||||||
<ul className="list-disc list-inside space-y-0.5">
|
<ul className="list-disc list-inside space-y-0.5">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable list of static items
|
|
||||||
<li key={i} className="text-xs text-gray-700">
|
<li key={i} className="text-xs text-gray-700">
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>
|
||||||
@@ -263,7 +262,6 @@ export function ComparisonTable({
|
|||||||
return (
|
return (
|
||||||
<ul className="list-disc list-inside space-y-0.5">
|
<ul className="list-disc list-inside space-y-0.5">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable list of static items
|
|
||||||
<li key={i} className="text-xs text-gray-700">
|
<li key={i} className="text-xs text-gray-700">
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ export function ItemCard({
|
|||||||
const duplicateItem = useDuplicateItem();
|
const duplicateItem = useDuplicateItem();
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
brand && name.startsWith(`${brand} `)
|
brand && name.startsWith(`${brand} `) ? name.slice(brand.length + 1) : name;
|
||||||
? name.slice(brand.length + 1)
|
|
||||||
: name;
|
|
||||||
|
|
||||||
const handleClick =
|
const handleClick =
|
||||||
linkTo === null
|
linkTo === null
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ export function useAdminGlobalItems(query?: string, tagNames?: string[]) {
|
|||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: ["admin-global-items", query ?? "", tagNames ?? []],
|
queryKey: ["admin-global-items", query ?? "", tagNames ?? []],
|
||||||
queryFn: ({ pageParam = 0 }) =>
|
queryFn: ({ pageParam = 0 }) =>
|
||||||
apiGet<AdminGlobalItemPage>(
|
apiGet<AdminGlobalItemPage>(`/api/admin/items?offset=${pageParam}&${qs}`),
|
||||||
`/api/admin/items?offset=${pageParam}&${qs}`,
|
|
||||||
),
|
|
||||||
getNextPageParam: (lastPage) =>
|
getNextPageParam: (lastPage) =>
|
||||||
lastPage.hasMore ? lastPage.nextOffset : undefined,
|
lastPage.hasMore ? lastPage.nextOffset : undefined,
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
@@ -79,8 +77,7 @@ export function useAdminGlobalItems(query?: string, tagNames?: string[]) {
|
|||||||
export function useAdminGlobalItem(id: number | null) {
|
export function useAdminGlobalItem(id: number | null) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["admin-global-item", id],
|
queryKey: ["admin-global-item", id],
|
||||||
queryFn: () =>
|
queryFn: () => apiGet<AdminGlobalItemDetail>(`/api/admin/items/${id}`),
|
||||||
apiGet<AdminGlobalItemDetail>(`/api/admin/items/${id}`),
|
|
||||||
enabled: id != null,
|
enabled: id != null,
|
||||||
retry: (count, error) =>
|
retry: (count, error) =>
|
||||||
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
||||||
@@ -90,13 +87,8 @@ export function useAdminGlobalItem(id: number | null) {
|
|||||||
export function useUpdateAdminGlobalItem() {
|
export function useUpdateAdminGlobalItem() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({ id, data }: { id: number; data: UpdateGlobalItemPayload }) =>
|
||||||
id,
|
apiPut<AdminGlobalItemDetail>(`/api/admin/items/${id}`, data),
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
id: number;
|
|
||||||
data: UpdateGlobalItemPayload;
|
|
||||||
}) => apiPut<AdminGlobalItemDetail>(`/api/admin/items/${id}`, data),
|
|
||||||
onSuccess: (_result, { id }) => {
|
onSuccess: (_result, { id }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["admin-global-items"] });
|
queryClient.invalidateQueries({ queryKey: ["admin-global-items"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["admin-global-item", id] });
|
queryClient.invalidateQueries({ queryKey: ["admin-global-item", id] });
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiDelete, apiGet, apiPost } from "../lib/api";
|
import { apiDelete, apiGet, apiPost } from "../lib/api";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
user: { id: string; email?: string; createdAt?: string; isAdmin?: boolean } | null;
|
user: {
|
||||||
|
id: string;
|
||||||
|
email?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
} | null;
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import { Route as UsersUserIdRouteImport } from './routes/users/$userId'
|
|||||||
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
|
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
|
||||||
import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId'
|
import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId'
|
||||||
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
|
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
|
||||||
|
import { Route as AdminTagsRouteImport } from './routes/admin/tags'
|
||||||
import { Route as AdminItemsRouteImport } from './routes/admin/items'
|
import { Route as AdminItemsRouteImport } from './routes/admin/items'
|
||||||
import { Route as ThreadsThreadIdIndexRouteImport } from './routes/threads/$threadId/index'
|
import { Route as ThreadsThreadIdIndexRouteImport } from './routes/threads/$threadId/index'
|
||||||
|
import { Route as AdminTagsTagIdRouteImport } from './routes/admin/tags.$tagId'
|
||||||
import { Route as AdminItemsItemIdRouteImport } from './routes/admin/items.$itemId'
|
import { Route as AdminItemsItemIdRouteImport } from './routes/admin/items.$itemId'
|
||||||
import { Route as ThreadsThreadIdCandidatesCandidateIdRouteImport } from './routes/threads/$threadId/candidates/$candidateId'
|
import { Route as ThreadsThreadIdCandidatesCandidateIdRouteImport } from './routes/threads/$threadId/candidates/$candidateId'
|
||||||
|
|
||||||
@@ -92,6 +94,11 @@ const GlobalItemsGlobalItemIdRoute = GlobalItemsGlobalItemIdRouteImport.update({
|
|||||||
path: '/global-items/$globalItemId',
|
path: '/global-items/$globalItemId',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AdminTagsRoute = AdminTagsRouteImport.update({
|
||||||
|
id: '/tags',
|
||||||
|
path: '/tags',
|
||||||
|
getParentRoute: () => AdminRoute,
|
||||||
|
} as any)
|
||||||
const AdminItemsRoute = AdminItemsRouteImport.update({
|
const AdminItemsRoute = AdminItemsRouteImport.update({
|
||||||
id: '/items',
|
id: '/items',
|
||||||
path: '/items',
|
path: '/items',
|
||||||
@@ -102,6 +109,11 @@ const ThreadsThreadIdIndexRoute = ThreadsThreadIdIndexRouteImport.update({
|
|||||||
path: '/threads/$threadId/',
|
path: '/threads/$threadId/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AdminTagsTagIdRoute = AdminTagsTagIdRouteImport.update({
|
||||||
|
id: '/$tagId',
|
||||||
|
path: '/$tagId',
|
||||||
|
getParentRoute: () => AdminTagsRoute,
|
||||||
|
} as any)
|
||||||
const AdminItemsItemIdRoute = AdminItemsItemIdRouteImport.update({
|
const AdminItemsItemIdRoute = AdminItemsItemIdRouteImport.update({
|
||||||
id: '/$itemId',
|
id: '/$itemId',
|
||||||
path: '/$itemId',
|
path: '/$itemId',
|
||||||
@@ -121,6 +133,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/settings': typeof SettingsRoute
|
'/settings': typeof SettingsRoute
|
||||||
'/admin/items': typeof AdminItemsRouteWithChildren
|
'/admin/items': typeof AdminItemsRouteWithChildren
|
||||||
|
'/admin/tags': typeof AdminTagsRouteWithChildren
|
||||||
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
||||||
'/items/$itemId': typeof ItemsItemIdRoute
|
'/items/$itemId': typeof ItemsItemIdRoute
|
||||||
'/setups/$setupId': typeof SetupsSetupIdRoute
|
'/setups/$setupId': typeof SetupsSetupIdRoute
|
||||||
@@ -130,6 +143,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/global-items/': typeof GlobalItemsIndexRoute
|
'/global-items/': typeof GlobalItemsIndexRoute
|
||||||
'/setups/': typeof SetupsIndexRoute
|
'/setups/': typeof SetupsIndexRoute
|
||||||
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
||||||
|
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
|
||||||
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
|
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
|
||||||
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
||||||
}
|
}
|
||||||
@@ -139,6 +153,7 @@ export interface FileRoutesByTo {
|
|||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/settings': typeof SettingsRoute
|
'/settings': typeof SettingsRoute
|
||||||
'/admin/items': typeof AdminItemsRouteWithChildren
|
'/admin/items': typeof AdminItemsRouteWithChildren
|
||||||
|
'/admin/tags': typeof AdminTagsRouteWithChildren
|
||||||
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
||||||
'/items/$itemId': typeof ItemsItemIdRoute
|
'/items/$itemId': typeof ItemsItemIdRoute
|
||||||
'/setups/$setupId': typeof SetupsSetupIdRoute
|
'/setups/$setupId': typeof SetupsSetupIdRoute
|
||||||
@@ -148,6 +163,7 @@ export interface FileRoutesByTo {
|
|||||||
'/global-items': typeof GlobalItemsIndexRoute
|
'/global-items': typeof GlobalItemsIndexRoute
|
||||||
'/setups': typeof SetupsIndexRoute
|
'/setups': typeof SetupsIndexRoute
|
||||||
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
||||||
|
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
|
||||||
'/threads/$threadId': typeof ThreadsThreadIdIndexRoute
|
'/threads/$threadId': typeof ThreadsThreadIdIndexRoute
|
||||||
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
||||||
}
|
}
|
||||||
@@ -159,6 +175,7 @@ export interface FileRoutesById {
|
|||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/settings': typeof SettingsRoute
|
'/settings': typeof SettingsRoute
|
||||||
'/admin/items': typeof AdminItemsRouteWithChildren
|
'/admin/items': typeof AdminItemsRouteWithChildren
|
||||||
|
'/admin/tags': typeof AdminTagsRouteWithChildren
|
||||||
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
||||||
'/items/$itemId': typeof ItemsItemIdRoute
|
'/items/$itemId': typeof ItemsItemIdRoute
|
||||||
'/setups/$setupId': typeof SetupsSetupIdRoute
|
'/setups/$setupId': typeof SetupsSetupIdRoute
|
||||||
@@ -168,6 +185,7 @@ export interface FileRoutesById {
|
|||||||
'/global-items/': typeof GlobalItemsIndexRoute
|
'/global-items/': typeof GlobalItemsIndexRoute
|
||||||
'/setups/': typeof SetupsIndexRoute
|
'/setups/': typeof SetupsIndexRoute
|
||||||
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
'/admin/items/$itemId': typeof AdminItemsItemIdRoute
|
||||||
|
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
|
||||||
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
|
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
|
||||||
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
||||||
}
|
}
|
||||||
@@ -180,6 +198,7 @@ export interface FileRouteTypes {
|
|||||||
| '/profile'
|
| '/profile'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/admin/items'
|
| '/admin/items'
|
||||||
|
| '/admin/tags'
|
||||||
| '/global-items/$globalItemId'
|
| '/global-items/$globalItemId'
|
||||||
| '/items/$itemId'
|
| '/items/$itemId'
|
||||||
| '/setups/$setupId'
|
| '/setups/$setupId'
|
||||||
@@ -189,6 +208,7 @@ export interface FileRouteTypes {
|
|||||||
| '/global-items/'
|
| '/global-items/'
|
||||||
| '/setups/'
|
| '/setups/'
|
||||||
| '/admin/items/$itemId'
|
| '/admin/items/$itemId'
|
||||||
|
| '/admin/tags/$tagId'
|
||||||
| '/threads/$threadId/'
|
| '/threads/$threadId/'
|
||||||
| '/threads/$threadId/candidates/$candidateId'
|
| '/threads/$threadId/candidates/$candidateId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
@@ -198,6 +218,7 @@ export interface FileRouteTypes {
|
|||||||
| '/profile'
|
| '/profile'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/admin/items'
|
| '/admin/items'
|
||||||
|
| '/admin/tags'
|
||||||
| '/global-items/$globalItemId'
|
| '/global-items/$globalItemId'
|
||||||
| '/items/$itemId'
|
| '/items/$itemId'
|
||||||
| '/setups/$setupId'
|
| '/setups/$setupId'
|
||||||
@@ -207,6 +228,7 @@ export interface FileRouteTypes {
|
|||||||
| '/global-items'
|
| '/global-items'
|
||||||
| '/setups'
|
| '/setups'
|
||||||
| '/admin/items/$itemId'
|
| '/admin/items/$itemId'
|
||||||
|
| '/admin/tags/$tagId'
|
||||||
| '/threads/$threadId'
|
| '/threads/$threadId'
|
||||||
| '/threads/$threadId/candidates/$candidateId'
|
| '/threads/$threadId/candidates/$candidateId'
|
||||||
id:
|
id:
|
||||||
@@ -217,6 +239,7 @@ export interface FileRouteTypes {
|
|||||||
| '/profile'
|
| '/profile'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/admin/items'
|
| '/admin/items'
|
||||||
|
| '/admin/tags'
|
||||||
| '/global-items/$globalItemId'
|
| '/global-items/$globalItemId'
|
||||||
| '/items/$itemId'
|
| '/items/$itemId'
|
||||||
| '/setups/$setupId'
|
| '/setups/$setupId'
|
||||||
@@ -226,6 +249,7 @@ export interface FileRouteTypes {
|
|||||||
| '/global-items/'
|
| '/global-items/'
|
||||||
| '/setups/'
|
| '/setups/'
|
||||||
| '/admin/items/$itemId'
|
| '/admin/items/$itemId'
|
||||||
|
| '/admin/tags/$tagId'
|
||||||
| '/threads/$threadId/'
|
| '/threads/$threadId/'
|
||||||
| '/threads/$threadId/candidates/$candidateId'
|
| '/threads/$threadId/candidates/$candidateId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
@@ -340,6 +364,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport
|
preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/admin/tags': {
|
||||||
|
id: '/admin/tags'
|
||||||
|
path: '/tags'
|
||||||
|
fullPath: '/admin/tags'
|
||||||
|
preLoaderRoute: typeof AdminTagsRouteImport
|
||||||
|
parentRoute: typeof AdminRoute
|
||||||
|
}
|
||||||
'/admin/items': {
|
'/admin/items': {
|
||||||
id: '/admin/items'
|
id: '/admin/items'
|
||||||
path: '/items'
|
path: '/items'
|
||||||
@@ -354,6 +385,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ThreadsThreadIdIndexRouteImport
|
preLoaderRoute: typeof ThreadsThreadIdIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/admin/tags/$tagId': {
|
||||||
|
id: '/admin/tags/$tagId'
|
||||||
|
path: '/$tagId'
|
||||||
|
fullPath: '/admin/tags/$tagId'
|
||||||
|
preLoaderRoute: typeof AdminTagsTagIdRouteImport
|
||||||
|
parentRoute: typeof AdminTagsRoute
|
||||||
|
}
|
||||||
'/admin/items/$itemId': {
|
'/admin/items/$itemId': {
|
||||||
id: '/admin/items/$itemId'
|
id: '/admin/items/$itemId'
|
||||||
path: '/$itemId'
|
path: '/$itemId'
|
||||||
@@ -383,13 +421,27 @@ const AdminItemsRouteWithChildren = AdminItemsRoute._addFileChildren(
|
|||||||
AdminItemsRouteChildren,
|
AdminItemsRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface AdminTagsRouteChildren {
|
||||||
|
AdminTagsTagIdRoute: typeof AdminTagsTagIdRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminTagsRouteChildren: AdminTagsRouteChildren = {
|
||||||
|
AdminTagsTagIdRoute: AdminTagsTagIdRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminTagsRouteWithChildren = AdminTagsRoute._addFileChildren(
|
||||||
|
AdminTagsRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface AdminRouteChildren {
|
interface AdminRouteChildren {
|
||||||
AdminItemsRoute: typeof AdminItemsRouteWithChildren
|
AdminItemsRoute: typeof AdminItemsRouteWithChildren
|
||||||
|
AdminTagsRoute: typeof AdminTagsRouteWithChildren
|
||||||
AdminIndexRoute: typeof AdminIndexRoute
|
AdminIndexRoute: typeof AdminIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdminRouteChildren: AdminRouteChildren = {
|
const AdminRouteChildren: AdminRouteChildren = {
|
||||||
AdminItemsRoute: AdminItemsRouteWithChildren,
|
AdminItemsRoute: AdminItemsRouteWithChildren,
|
||||||
|
AdminTagsRoute: AdminTagsRouteWithChildren,
|
||||||
AdminIndexRoute: AdminIndexRoute,
|
AdminIndexRoute: AdminIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Link, createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
Link,
|
||||||
|
Outlet,
|
||||||
|
useNavigate,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ function TagInput({
|
|||||||
{tag}
|
{tag}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => { e.stopPropagation(); removeTag(tag); }}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeTag(tag);
|
||||||
|
}}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -76,7 +79,9 @@ function TagInput({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={() => { if (inputValue) addTag(inputValue); }}
|
onBlur={() => {
|
||||||
|
if (inputValue) addTag(inputValue);
|
||||||
|
}}
|
||||||
placeholder={value.length === 0 ? "Add tags..." : ""}
|
placeholder={value.length === 0 ? "Add tags..." : ""}
|
||||||
className="outline-none bg-transparent text-sm flex-1 min-w-[100px]"
|
className="outline-none bg-transparent text-sm flex-1 min-w-[100px]"
|
||||||
/>
|
/>
|
||||||
@@ -91,7 +96,11 @@ function AdminItemEditPage() {
|
|||||||
const id = Number(itemId);
|
const id = Number(itemId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: item, isLoading, isError } = useAdminGlobalItem(isNaN(id) ? null : id);
|
const {
|
||||||
|
data: item,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useAdminGlobalItem(Number.isNaN(id) ? null : id);
|
||||||
const updateMutation = useUpdateAdminGlobalItem();
|
const updateMutation = useUpdateAdminGlobalItem();
|
||||||
const deleteMutation = useDeleteAdminGlobalItem();
|
const deleteMutation = useDeleteAdminGlobalItem();
|
||||||
|
|
||||||
@@ -121,7 +130,8 @@ function AdminItemEditPage() {
|
|||||||
model: item.model,
|
model: item.model,
|
||||||
category: item.category ?? "",
|
category: item.category ?? "",
|
||||||
weightGrams: item.weightGrams != null ? String(item.weightGrams) : "",
|
weightGrams: item.weightGrams != null ? String(item.weightGrams) : "",
|
||||||
priceCents: item.priceCents != null ? String(item.priceCents / 100) : "",
|
priceCents:
|
||||||
|
item.priceCents != null ? String(item.priceCents / 100) : "",
|
||||||
imageUrl: item.imageUrl ?? "",
|
imageUrl: item.imageUrl ?? "",
|
||||||
description: item.description ?? "",
|
description: item.description ?? "",
|
||||||
sourceUrl: item.sourceUrl ?? "",
|
sourceUrl: item.sourceUrl ?? "",
|
||||||
@@ -134,7 +144,9 @@ function AdminItemEditPage() {
|
|||||||
|
|
||||||
// Fetch manufacturers for dropdown
|
// Fetch manufacturers for dropdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<Manufacturer[]>("/api/manufacturers").then(setManufacturers).catch(() => {});
|
apiGet<Manufacturer[]>("/api/manufacturers")
|
||||||
|
.then(setManufacturers)
|
||||||
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleChange(
|
function handleChange(
|
||||||
@@ -146,7 +158,8 @@ function AdminItemEditPage() {
|
|||||||
|
|
||||||
async function handleSave(e: React.FormEvent) {
|
async function handleSave(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const weightGrams = form.weightGrams !== "" ? Number(form.weightGrams) : null;
|
const weightGrams =
|
||||||
|
form.weightGrams !== "" ? Number(form.weightGrams) : null;
|
||||||
const priceCents =
|
const priceCents =
|
||||||
form.priceCents !== "" ? Math.round(Number(form.priceCents) * 100) : null;
|
form.priceCents !== "" ? Math.round(Number(form.priceCents) * 100) : null;
|
||||||
|
|
||||||
@@ -184,7 +197,10 @@ function AdminItemEditPage() {
|
|||||||
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
|
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Array.from({ length: 8 }).map((_, i) => (
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
<div key={i} className="h-10 bg-gray-100 rounded-lg animate-pulse" />
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-10 bg-gray-100 rounded-lg animate-pulse"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +210,9 @@ function AdminItemEditPage() {
|
|||||||
if (isError || !item) {
|
if (isError || !item) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto text-center py-12">
|
<div className="max-w-2xl mx-auto text-center py-12">
|
||||||
<p className="text-sm text-red-500">Failed to load item. Please try again.</p>
|
<p className="text-sm text-red-500">
|
||||||
|
Failed to load item. Please try again.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -252,7 +270,9 @@ function AdminItemEditPage() {
|
|||||||
<label className={labelClass}>Brand (Manufacturer)</label>
|
<label className={labelClass}>Brand (Manufacturer)</label>
|
||||||
<select
|
<select
|
||||||
value={form.manufacturerId}
|
value={form.manufacturerId}
|
||||||
onChange={(e) => handleChange("manufacturerId", Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleChange("manufacturerId", Number(e.target.value))
|
||||||
|
}
|
||||||
className={`${inputClass} appearance-none bg-white`}
|
className={`${inputClass} appearance-none bg-white`}
|
||||||
>
|
>
|
||||||
<option value={0}>Select manufacturer...</option>
|
<option value={0}>Select manufacturer...</option>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ function AdminItemsPage() {
|
|||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
{allTags.map((tag) => (
|
{allTags.map((tag) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
onClick={() => toggleTag(tag.name)}
|
onClick={() => toggleTag(tag.name)}
|
||||||
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||||
@@ -150,25 +151,36 @@ function AdminItemsPage() {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer transition-colors"
|
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate({ to: "/admin/items/$itemId", params: { itemId: String(item.id) } })
|
navigate({
|
||||||
|
to: "/admin/items/$itemId",
|
||||||
|
params: { itemId: String(item.id) },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="font-medium text-gray-900">{item.brand}</span>
|
<span className="font-medium text-gray-900">
|
||||||
|
{item.brand}
|
||||||
|
</span>
|
||||||
<span className="text-gray-500 ml-1">{item.model}</span>
|
<span className="text-gray-500 ml-1">{item.model}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-700">
|
<td className="px-4 py-3 text-gray-700">
|
||||||
{item.category ?? <span className="text-gray-300">—</span>}
|
{item.category ?? (
|
||||||
|
<span className="text-gray-300">—</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-700">
|
<td className="px-4 py-3 text-gray-700">
|
||||||
{item.weightGrams != null
|
{item.weightGrams != null ? (
|
||||||
? formatWeight(item.weightGrams)
|
formatWeight(item.weightGrams)
|
||||||
: <span className="text-gray-300">—</span>}
|
) : (
|
||||||
|
<span className="text-gray-300">—</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-700">
|
<td className="px-4 py-3 text-gray-700">
|
||||||
{item.priceCents != null
|
{item.priceCents != null ? (
|
||||||
? formatPrice(item.priceCents)
|
formatPrice(item.priceCents)
|
||||||
: <span className="text-gray-300">—</span>}
|
) : (
|
||||||
|
<span className="text-gray-300">—</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{item.tags.length === 0 ? (
|
{item.tags.length === 0 ? (
|
||||||
@@ -209,7 +221,9 @@ function AdminItemsPage() {
|
|||||||
{/* Empty state (after load, no items) */}
|
{/* Empty state (after load, no items) */}
|
||||||
{!isLoading && allItems.length === 0 && !isError && (
|
{!isLoading && allItems.length === 0 && !isError && (
|
||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<p className="text-sm font-medium text-gray-900">No items found</p>
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
No items found
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-400 mt-1">
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
Try a different search or clear your filters.
|
Try a different search or clear your filters.
|
||||||
</p>
|
</p>
|
||||||
@@ -221,7 +235,9 @@ function AdminItemsPage() {
|
|||||||
|
|
||||||
{/* Loading more */}
|
{/* Loading more */}
|
||||||
{isFetchingNextPage && (
|
{isFetchingNextPage && (
|
||||||
<div className="py-4 text-center text-sm text-gray-400">Loading...</div>
|
<div className="py-4 text-center text-sm text-gray-400">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* All loaded message */}
|
{/* All loaded message */}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
type AdminTag,
|
||||||
useAdminTag,
|
useAdminTag,
|
||||||
useAdminTags,
|
useAdminTags,
|
||||||
useDeleteAdminTag,
|
useDeleteAdminTag,
|
||||||
useUpdateAdminTag,
|
useUpdateAdminTag,
|
||||||
type AdminTag,
|
|
||||||
} from "../../hooks/useAdminTags";
|
} from "../../hooks/useAdminTags";
|
||||||
|
|
||||||
export const Route = createFileRoute("/admin/tags/$tagId")({
|
export const Route = createFileRoute("/admin/tags/$tagId")({
|
||||||
@@ -53,12 +53,19 @@ function AdminTagEditPage() {
|
|||||||
const id = Number(tagId);
|
const id = Number(tagId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: tag, isLoading, isError } = useAdminTag(isNaN(id) ? null : id);
|
const {
|
||||||
|
data: tag,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useAdminTag(Number.isNaN(id) ? null : id);
|
||||||
const { data: allTags } = useAdminTags();
|
const { data: allTags } = useAdminTags();
|
||||||
const updateMutation = useUpdateAdminTag();
|
const updateMutation = useUpdateAdminTag();
|
||||||
const deleteMutation = useDeleteAdminTag();
|
const deleteMutation = useDeleteAdminTag();
|
||||||
|
|
||||||
const [form, setForm] = useState({ name: "", parentId: null as number | null });
|
const [form, setForm] = useState({
|
||||||
|
name: "",
|
||||||
|
parentId: null as number | null,
|
||||||
|
});
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -67,7 +74,10 @@ function AdminTagEditPage() {
|
|||||||
}
|
}
|
||||||
}, [tag]);
|
}, [tag]);
|
||||||
|
|
||||||
function handleChange(field: keyof typeof form, value: string | number | null) {
|
function handleChange(
|
||||||
|
field: keyof typeof form,
|
||||||
|
value: string | number | null,
|
||||||
|
) {
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +105,10 @@ function AdminTagEditPage() {
|
|||||||
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
|
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
<div key={i} className="h-10 bg-gray-100 rounded-lg animate-pulse" />
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-10 bg-gray-100 rounded-lg animate-pulse"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +118,9 @@ function AdminTagEditPage() {
|
|||||||
if (isError || !tag) {
|
if (isError || !tag) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto text-center py-12">
|
<div className="max-w-2xl mx-auto text-center py-12">
|
||||||
<p className="text-sm text-red-500">Failed to load tag. Please try again.</p>
|
<p className="text-sm text-red-500">
|
||||||
|
Failed to load tag. Please try again.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,7 +164,10 @@ function AdminTagEditPage() {
|
|||||||
<select
|
<select
|
||||||
value={form.parentId ?? ""}
|
value={form.parentId ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange("parentId", e.target.value ? Number(e.target.value) : null)
|
handleChange(
|
||||||
|
"parentId",
|
||||||
|
e.target.value ? Number(e.target.value) : null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className={`${inputClass} appearance-none bg-white`}
|
className={`${inputClass} appearance-none bg-white`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
type AdminTag,
|
||||||
useAdminTags,
|
useAdminTags,
|
||||||
useCreateAdminTag,
|
useCreateAdminTag,
|
||||||
type AdminTag,
|
|
||||||
} from "../../hooks/useAdminTags";
|
} from "../../hooks/useAdminTags";
|
||||||
import { LucideIcon } from "../../lib/iconData";
|
import { LucideIcon } from "../../lib/iconData";
|
||||||
|
|
||||||
@@ -287,7 +287,9 @@ function AdminTagsPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm font-medium text-gray-900">No tags yet</p>
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
No tags yet
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-400 mt-1">
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
Add your first tag using the form above.
|
Add your first tag using the form above.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ function ImportExportSection() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{importResult.errors.map((err, i) => (
|
{importResult.errors.map((err, i) => (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: static error list
|
|
||||||
<p key={i} className="text-red-600">
|
<p key={i} className="text-red-600">
|
||||||
{err}
|
{err}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ export const globalItems = pgTable(
|
|||||||
export const tags = pgTable("tags", {
|
export const tags = pgTable("tags", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
name: text("name").notNull().unique(),
|
name: text("name").notNull().unique(),
|
||||||
parentId: integer("parent_id").references(() => tags.id, { onDelete: "set null" }),
|
parentId: integer("parent_id").references(() => tags.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Context, Next } from "hono";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Context, Next } from "hono";
|
||||||
import { users } from "../../db/schema.ts";
|
import { users } from "../../db/schema.ts";
|
||||||
import { getOrCreateUser, verifyApiKey } from "../services/auth.service";
|
import { getOrCreateUser, verifyApiKey } from "../services/auth.service";
|
||||||
import { getOrCreateUncategorized } from "../services/category.service";
|
import { getOrCreateUncategorized } from "../services/category.service";
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ app.get("/", async (c) => {
|
|||||||
const result = await listGlobalItemsForAdmin(db, {
|
const result = await listGlobalItemsForAdmin(db, {
|
||||||
query: q || undefined,
|
query: q || undefined,
|
||||||
tagNames,
|
tagNames,
|
||||||
offset: isNaN(offset) ? 0 : offset,
|
offset: Number.isNaN(offset) ? 0 : offset,
|
||||||
limit: isNaN(limit) || limit > 100 ? 50 : limit,
|
limit: Number.isNaN(limit) || limit > 100 ? 50 : limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
@@ -62,10 +62,7 @@ app.get("/:id", async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// PUT /api/admin/items/:id — update item fields
|
// PUT /api/admin/items/:id — update item fields
|
||||||
app.put(
|
app.put("/:id", zValidator("json", updateGlobalItemAdminSchema), async (c) => {
|
||||||
"/:id",
|
|
||||||
zValidator("json", updateGlobalItemAdminSchema),
|
|
||||||
async (c) => {
|
|
||||||
const db = c.get("db");
|
const db = c.get("db");
|
||||||
const id = parseId(c.req.param("id"));
|
const id = parseId(c.req.param("id"));
|
||||||
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
||||||
@@ -73,8 +70,7 @@ app.put(
|
|||||||
const item = await updateGlobalItemById(db, id, data);
|
const item = await updateGlobalItemById(db, id, data);
|
||||||
if (!item) return c.json({ error: "Global item not found" }, 404);
|
if (!item) return c.json({ error: "Global item not found" }, 404);
|
||||||
return c.json(item);
|
return c.json(item);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// DELETE /api/admin/items/:id — delete item with FK cleanup
|
// DELETE /api/admin/items/:id — delete item with FK cleanup
|
||||||
app.delete("/:id", async (c) => {
|
app.delete("/:id", async (c) => {
|
||||||
|
|||||||
@@ -178,7 +178,12 @@ export async function listGlobalItemsForAdmin(
|
|||||||
})
|
})
|
||||||
.from(globalItemTags)
|
.from(globalItemTags)
|
||||||
.innerJoin(tags, eq(tags.id, globalItemTags.tagId))
|
.innerJoin(tags, eq(tags.id, globalItemTags.tagId))
|
||||||
.where(sql`${globalItemTags.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`);
|
.where(
|
||||||
|
sql`${globalItemTags.globalItemId} IN (${sql.join(
|
||||||
|
ids.map((id) => sql`${id}`),
|
||||||
|
sql`, `,
|
||||||
|
)})`,
|
||||||
|
);
|
||||||
|
|
||||||
const tagsByItemId = new Map<number, string[]>();
|
const tagsByItemId = new Map<number, string[]>();
|
||||||
for (const row of tagRows) {
|
for (const row of tagRows) {
|
||||||
@@ -194,7 +199,12 @@ export async function listGlobalItemsForAdmin(
|
|||||||
ownerCount: count(),
|
ownerCount: count(),
|
||||||
})
|
})
|
||||||
.from(items)
|
.from(items)
|
||||||
.where(sql`${items.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`)
|
.where(
|
||||||
|
sql`${items.globalItemId} IN (${sql.join(
|
||||||
|
ids.map((id) => sql`${id}`),
|
||||||
|
sql`, `,
|
||||||
|
)})`,
|
||||||
|
)
|
||||||
.groupBy(items.globalItemId);
|
.groupBy(items.globalItemId);
|
||||||
|
|
||||||
const ownerCountById = new Map<number, number>();
|
const ownerCountById = new Map<number, number>();
|
||||||
@@ -241,16 +251,22 @@ export async function updateGlobalItemById(
|
|||||||
|
|
||||||
// Build partial update — only set provided fields
|
// Build partial update — only set provided fields
|
||||||
const updateSet: Record<string, unknown> = {};
|
const updateSet: Record<string, unknown> = {};
|
||||||
if (fields.manufacturerId !== undefined) updateSet.manufacturerId = fields.manufacturerId;
|
if (fields.manufacturerId !== undefined)
|
||||||
|
updateSet.manufacturerId = fields.manufacturerId;
|
||||||
if (fields.model !== undefined) updateSet.model = fields.model;
|
if (fields.model !== undefined) updateSet.model = fields.model;
|
||||||
if ("category" in fields) updateSet.category = fields.category ?? null;
|
if ("category" in fields) updateSet.category = fields.category ?? null;
|
||||||
if ("weightGrams" in fields) updateSet.weightGrams = fields.weightGrams ?? null;
|
if ("weightGrams" in fields)
|
||||||
if ("priceCents" in fields) updateSet.priceCents = fields.priceCents ?? null;
|
updateSet.weightGrams = fields.weightGrams ?? null;
|
||||||
|
if ("priceCents" in fields)
|
||||||
|
updateSet.priceCents = fields.priceCents ?? null;
|
||||||
if ("imageUrl" in fields) updateSet.imageUrl = fields.imageUrl ?? null;
|
if ("imageUrl" in fields) updateSet.imageUrl = fields.imageUrl ?? null;
|
||||||
if ("description" in fields) updateSet.description = fields.description ?? null;
|
if ("description" in fields)
|
||||||
|
updateSet.description = fields.description ?? null;
|
||||||
if ("sourceUrl" in fields) updateSet.sourceUrl = fields.sourceUrl ?? null;
|
if ("sourceUrl" in fields) updateSet.sourceUrl = fields.sourceUrl ?? null;
|
||||||
if ("imageCredit" in fields) updateSet.imageCredit = fields.imageCredit ?? null;
|
if ("imageCredit" in fields)
|
||||||
if ("imageSourceUrl" in fields) updateSet.imageSourceUrl = fields.imageSourceUrl ?? null;
|
updateSet.imageCredit = fields.imageCredit ?? null;
|
||||||
|
if ("imageSourceUrl" in fields)
|
||||||
|
updateSet.imageSourceUrl = fields.imageSourceUrl ?? null;
|
||||||
|
|
||||||
let item: typeof globalItems.$inferSelect | undefined;
|
let item: typeof globalItems.$inferSelect | undefined;
|
||||||
if (Object.keys(updateSet).length > 0) {
|
if (Object.keys(updateSet).length > 0) {
|
||||||
@@ -295,14 +311,10 @@ export async function deleteGlobalItem(db: Db, id: number) {
|
|||||||
.where(eq(items.globalItemId, id));
|
.where(eq(items.globalItemId, id));
|
||||||
|
|
||||||
// 3. Remove tag associations (FK: globalItemTags.globalItemId → globalItems.id, no cascade)
|
// 3. Remove tag associations (FK: globalItemTags.globalItemId → globalItems.id, no cascade)
|
||||||
await tx
|
await tx.delete(globalItemTags).where(eq(globalItemTags.globalItemId, id));
|
||||||
.delete(globalItemTags)
|
|
||||||
.where(eq(globalItemTags.globalItemId, id));
|
|
||||||
|
|
||||||
// 4. Delete the global item
|
// 4. Delete the global item
|
||||||
await tx
|
await tx.delete(globalItems).where(eq(globalItems.id, id));
|
||||||
.delete(globalItems)
|
|
||||||
.where(eq(globalItems.id, id));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,11 @@ describe("Admin Tag Routes", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body).toMatchObject({ id: expect.any(Number), name: "ultralight", parentId: null });
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: "ultralight",
|
||||||
|
parentId: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates tag with parentId", async () => {
|
it("creates tag with parentId", async () => {
|
||||||
|
|||||||
@@ -536,9 +536,15 @@ describe("listGlobalItemsForAdmin", () => {
|
|||||||
|
|
||||||
it("filters by query string (brand/model)", async () => {
|
it("filters by query string (brand/model)", async () => {
|
||||||
const mfr = await insertManufacturer(db, "Salsa", "salsa");
|
const mfr = await insertManufacturer(db, "Salsa", "salsa");
|
||||||
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Woodsmoke 700" });
|
await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Woodsmoke 700",
|
||||||
|
});
|
||||||
const mfr2 = await insertManufacturer(db, "Apidura", "apidura");
|
const mfr2 = await insertManufacturer(db, "Apidura", "apidura");
|
||||||
await insertGlobalItem(db, { manufacturerId: mfr2.id, model: "Racing Saddle Bag" });
|
await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr2.id,
|
||||||
|
model: "Racing Saddle Bag",
|
||||||
|
});
|
||||||
|
|
||||||
const result = await listGlobalItemsForAdmin(db, { query: "salsa" });
|
const result = await listGlobalItemsForAdmin(db, { query: "salsa" });
|
||||||
expect(result.items).toHaveLength(1);
|
expect(result.items).toHaveLength(1);
|
||||||
@@ -547,7 +553,10 @@ describe("listGlobalItemsForAdmin", () => {
|
|||||||
|
|
||||||
it("includes tags and ownerCount per item", async () => {
|
it("includes tags and ownerCount per item", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Test Item" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Test Item",
|
||||||
|
});
|
||||||
const tag = await insertTag(db, "bikepacking");
|
const tag = await insertTag(db, "bikepacking");
|
||||||
await tagGlobalItem(db, globalItem.id, tag.id!);
|
await tagGlobalItem(db, globalItem.id, tag.id!);
|
||||||
|
|
||||||
@@ -556,7 +565,9 @@ describe("listGlobalItemsForAdmin", () => {
|
|||||||
.insert(schema.users)
|
.insert(schema.users)
|
||||||
.values({ logtoSub: "test-sub" })
|
.values({ logtoSub: "test-sub" })
|
||||||
.returning();
|
.returning();
|
||||||
await insertItem(db, "My Test Item", user!.id, { globalItemId: globalItem.id });
|
await insertItem(db, "My Test Item", user!.id, {
|
||||||
|
globalItemId: globalItem.id,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await listGlobalItemsForAdmin(db);
|
const result = await listGlobalItemsForAdmin(db);
|
||||||
expect(result.items).toHaveLength(1);
|
expect(result.items).toHaveLength(1);
|
||||||
@@ -579,7 +590,10 @@ describe("updateGlobalItemById", () => {
|
|||||||
|
|
||||||
it("updates model field by id", async () => {
|
it("updates model field by id", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Original" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Original",
|
||||||
|
});
|
||||||
|
|
||||||
await updateGlobalItemById(db, globalItem.id, { model: "Updated" });
|
await updateGlobalItemById(db, globalItem.id, { model: "Updated" });
|
||||||
|
|
||||||
@@ -589,9 +603,14 @@ describe("updateGlobalItemById", () => {
|
|||||||
|
|
||||||
it("syncs tags when tags array provided", async () => {
|
it("syncs tags when tags array provided", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Item" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Tagged Item",
|
||||||
|
});
|
||||||
|
|
||||||
await updateGlobalItemById(db, globalItem.id, { tags: ["cycling", "gravel"] });
|
await updateGlobalItemById(db, globalItem.id, {
|
||||||
|
tags: ["cycling", "gravel"],
|
||||||
|
});
|
||||||
|
|
||||||
const result = await listGlobalItemsForAdmin(db);
|
const result = await listGlobalItemsForAdmin(db);
|
||||||
const found = result.items.find((i) => i.id === globalItem.id);
|
const found = result.items.find((i) => i.id === globalItem.id);
|
||||||
@@ -614,7 +633,10 @@ describe("deleteGlobalItem", () => {
|
|||||||
|
|
||||||
it("deletes item and returns true", async () => {
|
it("deletes item and returns true", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "To Delete" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "To Delete",
|
||||||
|
});
|
||||||
|
|
||||||
const result = await deleteGlobalItem(db, globalItem.id);
|
const result = await deleteGlobalItem(db, globalItem.id);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
@@ -625,12 +647,17 @@ describe("deleteGlobalItem", () => {
|
|||||||
|
|
||||||
it("nullifies items.globalItemId before deleting", async () => {
|
it("nullifies items.globalItemId before deleting", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Owned Item" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Owned Item",
|
||||||
|
});
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.insert(schema.users)
|
.insert(schema.users)
|
||||||
.values({ logtoSub: "delete-test-sub" })
|
.values({ logtoSub: "delete-test-sub" })
|
||||||
.returning();
|
.returning();
|
||||||
const userItem = await insertItem(db, "User Item", user!.id, { globalItemId: globalItem.id });
|
const userItem = await insertItem(db, "User Item", user!.id, {
|
||||||
|
globalItemId: globalItem.id,
|
||||||
|
});
|
||||||
|
|
||||||
await deleteGlobalItem(db, globalItem.id);
|
await deleteGlobalItem(db, globalItem.id);
|
||||||
|
|
||||||
@@ -643,7 +670,10 @@ describe("deleteGlobalItem", () => {
|
|||||||
|
|
||||||
it("removes globalItemTags before deleting", async () => {
|
it("removes globalItemTags before deleting", async () => {
|
||||||
const mfr = await insertManufacturer(db);
|
const mfr = await insertManufacturer(db);
|
||||||
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Delete" });
|
const globalItem = await insertGlobalItem(db, {
|
||||||
|
manufacturerId: mfr.id,
|
||||||
|
model: "Tagged Delete",
|
||||||
|
});
|
||||||
const tag = await insertTag(db, "delete-tag");
|
const tag = await insertTag(db, "delete-tag");
|
||||||
await tagGlobalItem(db, globalItem.id, tag.id!);
|
await tagGlobalItem(db, globalItem.id, tag.id!);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { beforeEach, describe, expect, it } from "bun:test";
|
import { beforeEach, describe, expect, it } from "bun:test";
|
||||||
import { manufacturers } from "../../src/db/schema.ts";
|
|
||||||
import {
|
import {
|
||||||
createManufacturer,
|
createManufacturer,
|
||||||
getManufacturerBySlug,
|
getManufacturerBySlug,
|
||||||
|
|||||||
@@ -5,16 +5,11 @@ import {
|
|||||||
deleteTag,
|
deleteTag,
|
||||||
getAdminTags,
|
getAdminTags,
|
||||||
getAllTags,
|
getAllTags,
|
||||||
getTagWithCounts,
|
|
||||||
updateTag,
|
updateTag,
|
||||||
} from "../../src/server/services/tag.service.ts";
|
} from "../../src/server/services/tag.service.ts";
|
||||||
import { createTestDb } from "../helpers/db.ts";
|
import { createTestDb } from "../helpers/db.ts";
|
||||||
|
|
||||||
async function insertTag(
|
async function insertTag(db: any, name: string, parentId?: number | null) {
|
||||||
db: any,
|
|
||||||
name: string,
|
|
||||||
parentId?: number | null,
|
|
||||||
) {
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(tags)
|
.insert(tags)
|
||||||
.values({ name, parentId: parentId ?? null })
|
.values({ name, parentId: parentId ?? null })
|
||||||
@@ -108,7 +103,10 @@ describe("createTag", () => {
|
|||||||
|
|
||||||
it("creates a tag with parentId set to an existing tag id", async () => {
|
it("creates a tag with parentId set to an existing tag id", async () => {
|
||||||
const parent = await createTag(db, { name: "gear" });
|
const parent = await createTag(db, { name: "gear" });
|
||||||
const child = await createTag(db, { name: "clothing", parentId: parent.id });
|
const child = await createTag(db, {
|
||||||
|
name: "clothing",
|
||||||
|
parentId: parent.id,
|
||||||
|
});
|
||||||
expect(child.parentId).toBe(parent.id);
|
expect(child.parentId).toBe(parent.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -175,7 +173,7 @@ describe("deleteTag", () => {
|
|||||||
const parent = await insertTag(db, "parent");
|
const parent = await insertTag(db, "parent");
|
||||||
const child = await insertTag(db, "child", parent.id);
|
const child = await insertTag(db, "child", parent.id);
|
||||||
await deleteTag(db, parent.id);
|
await deleteTag(db, parent.id);
|
||||||
const [childRow] = await db
|
const [_childRow] = await db
|
||||||
.select({ parentId: tags.parentId })
|
.select({ parentId: tags.parentId })
|
||||||
.from(tags)
|
.from(tags)
|
||||||
.where((t: any) => t.id === child.id);
|
.where((t: any) => t.id === child.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user