Add ESLint + Prettier + EditorConfig tooling at repo root

v1.0 Phase 1.1 — repo-wide lint/format baseline.

- eslint.config.mjs (flat config) lints server, client, shared
- .prettierrc.json, .prettierignore, .editorconfig, .nvmrc
- Root package.json holds shared devDeps; per-package scripts keep
  their typecheck + test runners
- Fix 7 lint issues surfaced by the baseline run:
  - TicketDetail.tsx: replace ternary-with-side-effects with if/else
  - admin/Users.tsx: escape apostrophe in JSX
  - errorHandler.ts: typed err as unknown with ErrorLike refinement
  - users.ts: Prisma.UserUpdateInput instead of Record<string, any>
  - seed.ts: drop unused goddard binding
- Run prettier across tracked sources for a clean formatting baseline
This commit is contained in:
2026-04-18 14:47:34 -04:00
parent 2a6090e473
commit 27d2ab0f0d
48 changed files with 14460 additions and 1096 deletions
+83 -81
View File
@@ -1,13 +1,13 @@
import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { Search, ChevronRight, X } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import api from '../api/client'
import Layout from '../components/Layout'
import SeverityBadge from '../components/SeverityBadge'
import StatusBadge from '../components/StatusBadge'
import Avatar from '../components/Avatar'
import { Ticket, TicketStatus, Category, CTIType, Item } from '../types'
import { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { Search, ChevronRight, X } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import api from '../api/client';
import Layout from '../components/Layout';
import SeverityBadge from '../components/SeverityBadge';
import StatusBadge from '../components/StatusBadge';
import Avatar from '../components/Avatar';
import { Ticket, TicketStatus, Category, CTIType, Item } from '../types';
const STATUSES: { value: TicketStatus | ''; label: string }[] = [
{ value: '', label: 'All Statuses' },
@@ -15,97 +15,95 @@ const STATUSES: { value: TicketStatus | ''; label: string }[] = [
{ value: 'IN_PROGRESS', label: 'In Progress' },
{ value: 'RESOLVED', label: 'Resolved' },
{ value: 'CLOSED', label: 'Closed' },
]
];
const selectClass =
'bg-gray-800 border border-gray-700 text-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'
'bg-gray-800 border border-gray-700 text-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';
// Queue label built from whatever CTI level is selected
function queueLabel(
category: Category | null,
type: CTIType | null,
item: Item | null,
): string {
if (item && type && category) return `${category.name} ${type.name} ${item.name}`
if (type && category) return `${category.name} ${type.name}`
if (category) return category.name
return ''
function queueLabel(category: Category | null, type: CTIType | null, item: Item | null): string {
if (item && type && category) return `${category.name} ${type.name} ${item.name}`;
if (type && category) return `${category.name} ${type.name}`;
if (category) return category.name;
return '';
}
export default function Dashboard() {
const [tickets, setTickets] = useState<Ticket[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [status, setStatus] = useState<TicketStatus | ''>('')
const [severity, setSeverity] = useState('')
const [tickets, setTickets] = useState<Ticket[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [status, setStatus] = useState<TicketStatus | ''>('');
const [severity, setSeverity] = useState('');
// CTI queue filter state
const [categories, setCategories] = useState<Category[]>([])
const [types, setTypes] = useState<CTIType[]>([])
const [items, setItems] = useState<Item[]>([])
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null)
const [selectedType, setSelectedType] = useState<CTIType | null>(null)
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
const [showQueueFilter, setShowQueueFilter] = useState(false)
const [categories, setCategories] = useState<Category[]>([]);
const [types, setTypes] = useState<CTIType[]>([]);
const [items, setItems] = useState<Item[]>([]);
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
const [selectedType, setSelectedType] = useState<CTIType | null>(null);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
const [showQueueFilter, setShowQueueFilter] = useState(false);
useEffect(() => {
api.get<Category[]>('/cti/categories').then((r) => setCategories(r.data))
}, [])
api.get<Category[]>('/cti/categories').then((r) => setCategories(r.data));
}, []);
const handleCategorySelect = (cat: Category) => {
setSelectedCategory(cat)
setSelectedType(null)
setSelectedItem(null)
setTypes([])
setItems([])
api.get<CTIType[]>('/cti/types', { params: { categoryId: cat.id } }).then((r) => setTypes(r.data))
}
setSelectedCategory(cat);
setSelectedType(null);
setSelectedItem(null);
setTypes([]);
setItems([]);
api
.get<CTIType[]>('/cti/types', { params: { categoryId: cat.id } })
.then((r) => setTypes(r.data));
};
const handleTypeSelect = (type: CTIType) => {
setSelectedType(type)
setSelectedItem(null)
setItems([])
api.get<Item[]>('/cti/items', { params: { typeId: type.id } }).then((r) => setItems(r.data))
}
setSelectedType(type);
setSelectedItem(null);
setItems([]);
api.get<Item[]>('/cti/items', { params: { typeId: type.id } }).then((r) => setItems(r.data));
};
const handleItemSelect = (item: Item) => {
setSelectedItem(item)
setShowQueueFilter(false)
}
setSelectedItem(item);
setShowQueueFilter(false);
};
const clearQueue = () => {
setSelectedCategory(null)
setSelectedType(null)
setSelectedItem(null)
setTypes([])
setItems([])
}
setSelectedCategory(null);
setSelectedType(null);
setSelectedItem(null);
setTypes([]);
setItems([]);
};
// Derive the most specific filter param
const queueParams: Record<string, string> = {}
if (selectedItem) queueParams.itemId = selectedItem.id
else if (selectedType) queueParams.typeId = selectedType.id
else if (selectedCategory) queueParams.categoryId = selectedCategory.id
const queueParams: Record<string, string> = {};
if (selectedItem) queueParams.itemId = selectedItem.id;
else if (selectedType) queueParams.typeId = selectedType.id;
else if (selectedCategory) queueParams.categoryId = selectedCategory.id;
const fetchTickets = useCallback(() => {
setLoading(true)
const params: Record<string, string> = { ...queueParams }
if (status) params.status = status
if (severity) params.severity = severity
if (search) params.search = search
setLoading(true);
const params: Record<string, string> = { ...queueParams };
if (status) params.status = status;
if (severity) params.severity = severity;
if (search) params.search = search;
api
.get<Ticket[]>('/tickets', { params })
.then((r) => setTickets(r.data))
.finally(() => setLoading(false))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, severity, search, selectedCategory, selectedType, selectedItem])
.finally(() => setLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, severity, search, selectedCategory, selectedType, selectedItem]);
useEffect(() => {
const t = setTimeout(fetchTickets, 300)
return () => clearTimeout(t)
}, [fetchTickets])
const t = setTimeout(fetchTickets, 300);
return () => clearTimeout(t);
}, [fetchTickets]);
const activeQueue = queueLabel(selectedCategory, selectedType, selectedItem)
const activeQueue = queueLabel(selectedCategory, selectedType, selectedItem);
return (
<Layout title="All Tickets">
@@ -161,7 +159,10 @@ export default function Dashboard() {
<>
<span className="max-w-48 truncate">{activeQueue}</span>
<span
onClick={(e) => { e.stopPropagation(); clearQueue() }}
onClick={(e) => {
e.stopPropagation();
clearQueue();
}}
className="text-blue-400 hover:text-white transition-colors cursor-pointer"
>
<X size={13} />
@@ -173,7 +174,8 @@ export default function Dashboard() {
</button>
{showQueueFilter && (
<div className="absolute z-20 top-full mt-1 left-0 bg-gray-900 border border-gray-700 rounded-xl shadow-2xl overflow-hidden flex"
<div
className="absolute z-20 top-full mt-1 left-0 bg-gray-900 border border-gray-700 rounded-xl shadow-2xl overflow-hidden flex"
style={{ minWidth: '520px' }}
>
{/* Categories */}
@@ -278,12 +280,12 @@ export default function Dashboard() {
ticket.severity === 1
? 'bg-red-500'
: ticket.severity === 2
? 'bg-orange-400'
: ticket.severity === 3
? 'bg-yellow-400'
: ticket.severity === 4
? 'bg-blue-400'
: 'bg-gray-600'
? 'bg-orange-400'
: ticket.severity === 3
? 'bg-yellow-400'
: ticket.severity === 4
? 'bg-blue-400'
: 'bg-gray-600'
}`}
/>
@@ -324,5 +326,5 @@ export default function Dashboard() {
</div>
)}
</Layout>
)
);
}