Compare commits
10 Commits
15f146ee89
...
feature/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| fb925a9dce | |||
| 70e7cd2f0f | |||
| 33f735af67 | |||
| f8a1a00e0a | |||
| 27c36b6b9a | |||
| 684cfd3789 | |||
| 52751ae9d4 | |||
| 3fc737c872 | |||
| b993a0a831 | |||
| a8696c2a85 |
@@ -2,9 +2,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [Develop]
|
||||
pull_request:
|
||||
branches: [Develop]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
|
||||
663
drizzle/meta/0008_snapshot.json
Normal file
663
drizzle/meta/0008_snapshot.json
Normal file
@@ -0,0 +1,663 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ede9f482-7af0-42bc-9672-43f5fba289d0",
|
||||
"prevId": "738e67c5-ebad-46c1-9261-6ab60ec4bdb1",
|
||||
"tables": {
|
||||
"api_keys": {
|
||||
"name": "api_keys",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_hash": {
|
||||
"name": "key_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_prefix": {
|
||||
"name": "key_prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"categories": {
|
||||
"name": "categories",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'package'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"categories_name_unique": {
|
||||
"name": "categories_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"items": {
|
||||
"name": "items",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"weight_grams": {
|
||||
"name": "weight_grams",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"price_cents": {
|
||||
"name": "price_cents",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"product_url": {
|
||||
"name": "product_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_filename": {
|
||||
"name": "image_filename",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_source_url": {
|
||||
"name": "image_source_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"items_category_id_categories_id_fk": {
|
||||
"name": "items_category_id_categories_id_fk",
|
||||
"tableFrom": "items",
|
||||
"tableTo": "categories",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"setup_items": {
|
||||
"name": "setup_items",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"setup_id": {
|
||||
"name": "setup_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"classification": {
|
||||
"name": "classification",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'base'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"setup_items_setup_id_setups_id_fk": {
|
||||
"name": "setup_items_setup_id_setups_id_fk",
|
||||
"tableFrom": "setup_items",
|
||||
"tableTo": "setups",
|
||||
"columnsFrom": [
|
||||
"setup_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"setup_items_item_id_items_id_fk": {
|
||||
"name": "setup_items_item_id_items_id_fk",
|
||||
"tableFrom": "setup_items",
|
||||
"tableTo": "items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"setups": {
|
||||
"name": "setups",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"thread_candidates": {
|
||||
"name": "thread_candidates",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"thread_id": {
|
||||
"name": "thread_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"weight_grams": {
|
||||
"name": "weight_grams",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"price_cents": {
|
||||
"name": "price_cents",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"product_url": {
|
||||
"name": "product_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_filename": {
|
||||
"name": "image_filename",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_source_url": {
|
||||
"name": "image_source_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'researching'"
|
||||
},
|
||||
"pros": {
|
||||
"name": "pros",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cons": {
|
||||
"name": "cons",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"thread_candidates_thread_id_threads_id_fk": {
|
||||
"name": "thread_candidates_thread_id_threads_id_fk",
|
||||
"tableFrom": "thread_candidates",
|
||||
"tableTo": "threads",
|
||||
"columnsFrom": [
|
||||
"thread_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"thread_candidates_category_id_categories_id_fk": {
|
||||
"name": "thread_candidates_category_id_categories_id_fk",
|
||||
"tableFrom": "thread_candidates",
|
||||
"tableTo": "categories",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"threads": {
|
||||
"name": "threads",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'active'"
|
||||
},
|
||||
"resolved_candidate_id": {
|
||||
"name": "resolved_candidate_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"threads_category_id_categories_id_fk": {
|
||||
"name": "threads_category_id_categories_id_fk",
|
||||
"tableFrom": "threads",
|
||||
"tableTo": "categories",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,13 @@
|
||||
"when": 1775215076284,
|
||||
"tag": "0007_icy_prodigy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1775232090363,
|
||||
"tag": "0008_loving_colossus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Reorder, useDragControls } from "framer-motion";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { useFormatters } from "../hooks/useFormatters";
|
||||
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
||||
import { LucideIcon } from "../lib/iconData";
|
||||
@@ -31,6 +32,7 @@ interface CandidateListItemProps {
|
||||
isActive: boolean;
|
||||
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
|
||||
delta?: CandidateDelta;
|
||||
onDragEnd?: () => void;
|
||||
}
|
||||
|
||||
const RANK_COLORS = ["#D4AF37", "#C0C0C0", "#CD7F32"]; // gold, silver, bronze
|
||||
@@ -53,8 +55,9 @@ export function CandidateListItem({
|
||||
isActive,
|
||||
onStatusChange,
|
||||
delta,
|
||||
onDragEnd,
|
||||
}: CandidateListItemProps) {
|
||||
const controls = useDragControls();
|
||||
const isDragging = useRef(false);
|
||||
const { weight, price } = useFormatters();
|
||||
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
||||
const openConfirmDeleteCandidate = useUIStore(
|
||||
@@ -64,20 +67,15 @@ export function CandidateListItem({
|
||||
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
||||
|
||||
const sharedClassName =
|
||||
"flex items-center gap-3 bg-white rounded-xl border border-gray-100 p-3 hover:border-gray-200 hover:shadow-sm transition-all group cursor-default";
|
||||
"flex items-center gap-3 bg-white rounded-xl border border-gray-100 p-3 hover:border-gray-200 hover:shadow-sm group cursor-default";
|
||||
|
||||
const innerContent = (
|
||||
<>
|
||||
{/* Drag handle */}
|
||||
{/* Drag handle indicator */}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
onPointerDown={(e) => controls.start(e)}
|
||||
className="cursor-grab active:cursor-grabbing text-gray-300 hover:text-gray-500 touch-none shrink-0"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<span className="text-gray-300 shrink-0">
|
||||
<LucideIcon name="grip-vertical" size={16} />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Rank badge */}
|
||||
@@ -103,7 +101,10 @@ export function CandidateListItem({
|
||||
{/* Name + badges */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCandidateEditPanel(candidate.id)}
|
||||
onClick={() => {
|
||||
if (isDragging.current) return;
|
||||
openCandidateEditPanel(candidate.id);
|
||||
}}
|
||||
className="flex-1 min-w-0 text-left"
|
||||
>
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||
@@ -215,8 +216,17 @@ export function CandidateListItem({
|
||||
return (
|
||||
<Reorder.Item
|
||||
value={candidate}
|
||||
dragControls={controls}
|
||||
dragListener={false}
|
||||
onDragStart={() => {
|
||||
isDragging.current = true;
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setTimeout(() => {
|
||||
isDragging.current = false;
|
||||
}, 0);
|
||||
onDragEnd?.();
|
||||
}}
|
||||
whileDrag={{ cursor: "grabbing" }}
|
||||
style={{ marginBottom: 8, cursor: "grab" }}
|
||||
className={sharedClassName}
|
||||
>
|
||||
{innerContent}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CreateItem } from "../../shared/types";
|
||||
import { apiDelete, apiGet, apiPost, apiPut, apiUpload } from "../lib/api";
|
||||
import {
|
||||
ApiError,
|
||||
apiDelete,
|
||||
apiGet,
|
||||
apiPost,
|
||||
apiPut,
|
||||
apiUpload,
|
||||
} from "../lib/api";
|
||||
|
||||
interface Item {
|
||||
id: number;
|
||||
@@ -45,6 +52,8 @@ export function useItem(id: number | null) {
|
||||
queryKey: ["items", id],
|
||||
queryFn: () => apiGet<ItemWithCategory>(`/api/items/${id}`),
|
||||
enabled: id != null,
|
||||
retry: (count, error) =>
|
||||
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiDelete, apiGet, apiPatch, apiPost, apiPut } from "../lib/api";
|
||||
import {
|
||||
ApiError,
|
||||
apiDelete,
|
||||
apiGet,
|
||||
apiPatch,
|
||||
apiPost,
|
||||
apiPut,
|
||||
} from "../lib/api";
|
||||
|
||||
interface SetupListItem {
|
||||
id: number;
|
||||
@@ -50,6 +57,8 @@ export function useSetup(setupId: number | null) {
|
||||
queryKey: ["setups", setupId],
|
||||
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}`),
|
||||
enabled: setupId != null,
|
||||
retry: (count, error) =>
|
||||
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiDelete, apiGet, apiPost, apiPut } from "../lib/api";
|
||||
import { ApiError, apiDelete, apiGet, apiPost, apiPut } from "../lib/api";
|
||||
|
||||
interface ThreadListItem {
|
||||
id: number;
|
||||
@@ -61,6 +61,8 @@ export function useThread(threadId: number | null) {
|
||||
queryKey: ["threads", threadId],
|
||||
queryFn: () => apiGet<ThreadWithCandidates>(`/api/threads/${threadId}`),
|
||||
enabled: threadId != null,
|
||||
retry: (count, error) =>
|
||||
error instanceof ApiError && error.status === 404 ? false : count < 3,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class ApiError extends Error {
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { z } from "zod";
|
||||
@@ -16,6 +16,11 @@ export const Route = createFileRoute("/collection/")({
|
||||
});
|
||||
|
||||
const TAB_ORDER = ["gear", "planning", "setups"] as const;
|
||||
const TAB_LABELS: Record<(typeof TAB_ORDER)[number], string> = {
|
||||
gear: "Gear",
|
||||
planning: "Planning",
|
||||
setups: "Setups",
|
||||
};
|
||||
|
||||
const slideVariants = {
|
||||
enter: (dir: number) => ({ x: `${dir * 15}%`, opacity: 0 }),
|
||||
@@ -33,6 +38,26 @@ function CollectionPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 overflow-x-hidden">
|
||||
{/* Tab navigation */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="flex bg-gray-100 rounded-full p-0.5 gap-0.5">
|
||||
{TAB_ORDER.map((t) => (
|
||||
<Link
|
||||
key={t}
|
||||
to="/collection"
|
||||
search={{ tab: t }}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-full transition-colors ${
|
||||
tab === t
|
||||
? "bg-gray-700 text-white"
|
||||
: "text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{TAB_LABELS[t]}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait" initial={false} custom={direction}>
|
||||
<motion.div
|
||||
key={tab}
|
||||
|
||||
@@ -36,12 +36,11 @@ function ThreadDetailPage() {
|
||||
thread?.categoryId ?? 0,
|
||||
);
|
||||
|
||||
const [tempItems, setTempItems] =
|
||||
useState<typeof thread extends { candidates: infer C } ? C : never | null>(
|
||||
null,
|
||||
);
|
||||
const [tempItems, setTempItems] = useState<
|
||||
NonNullable<typeof thread>["candidates"] | null
|
||||
>(null);
|
||||
|
||||
// Clear tempItems when server data changes (biome-ignore: thread?.candidates is intentional dep)
|
||||
// Clear tempItems when server data changes
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: thread?.candidates is the intended trigger
|
||||
useEffect(() => {
|
||||
setTempItems(null);
|
||||
@@ -88,10 +87,9 @@ function ThreadDetailPage() {
|
||||
|
||||
function handleDragEnd() {
|
||||
if (!tempItems) return;
|
||||
reorderMutation.mutate(
|
||||
{ orderedIds: tempItems.map((c) => c.id) },
|
||||
{ onSettled: () => setTempItems(null) },
|
||||
);
|
||||
reorderMutation.mutate({
|
||||
orderedIds: tempItems.map((c) => c.id),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -131,7 +129,7 @@ function ThreadDetailPage() {
|
||||
|
||||
{/* Toolbar: Add candidate + view toggle */}
|
||||
<div className="mb-6 flex items-center gap-3 flex-wrap">
|
||||
{isActive && candidateViewMode !== "compare" && (
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCandidateAddPanel}
|
||||
@@ -227,8 +225,7 @@ function ThreadDetailPage() {
|
||||
axis="y"
|
||||
values={displayItems}
|
||||
onReorder={setTempItems}
|
||||
onPointerUp={handleDragEnd}
|
||||
className="flex flex-col gap-2"
|
||||
className="flex flex-col"
|
||||
>
|
||||
{displayItems.map((candidate, index) => (
|
||||
<CandidateListItem
|
||||
@@ -243,6 +240,7 @@ function ThreadDetailPage() {
|
||||
})
|
||||
}
|
||||
delta={deltas[candidate.id]}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getAllItems(db: Db = prodDb) {
|
||||
name: items.name,
|
||||
weightGrams: items.weightGrams,
|
||||
priceCents: items.priceCents,
|
||||
quantity: items.quantity,
|
||||
categoryId: items.categoryId,
|
||||
notes: items.notes,
|
||||
productUrl: items.productUrl,
|
||||
@@ -63,6 +64,7 @@ export function createItem(
|
||||
name: data.name,
|
||||
weightGrams: data.weightGrams ?? null,
|
||||
priceCents: data.priceCents ?? null,
|
||||
quantity: data.quantity ?? 1,
|
||||
categoryId: data.categoryId,
|
||||
notes: data.notes ?? null,
|
||||
productUrl: data.productUrl ?? null,
|
||||
@@ -80,6 +82,7 @@ export function updateItem(
|
||||
name: string;
|
||||
weightGrams: number;
|
||||
priceCents: number;
|
||||
quantity: number;
|
||||
categoryId: number;
|
||||
notes: string;
|
||||
productUrl: string;
|
||||
|
||||
Reference in New Issue
Block a user