npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@propriety/court-calendar

v1.0.124

Published

A React component library for managing court dates, cases, and evidence. Built with FullCalendar, MUI, and Dexie (IndexedDB caching).

Readme

@propriety/court-calendar

A React component library for managing court dates, cases, and evidence. Built with FullCalendar, MUI, and Dexie (IndexedDB caching).

Installation

npm install @propriety/court-calendar

Peer dependencies:

npm install react react-dom @mui/material @mui/icons-material @mui/x-data-grid @mui/x-date-pickers @emotion/react @emotion/styled

Usage

import { CCalendar } from '@propriety/court-calendar';
import '@propriety/court-calendar/styles.css';

function App() {
    return <CCalendar apiKey="your-api-key" activeUser={1} />;
}

CSS import

You must import the stylesheet separately:

import '@propriety/court-calendar/styles.css';

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | apiKey | string | required | API key for authenticating with the Aventine API | | activeUser | number | required | The current user's ID (used for chair assignments and edit tracking) | | showLegend | boolean | true | Whether to render the hearing-type legend below the calendar | | mode | 'light' \| 'dark' | 'light' | Color scheme | | themeOverrides | ThemeOptions | — | MUI theme overrides applied on top of the default theme | | routeParams | CalendarRouteParams | — | Current URL state, parsed and passed in by the host app's router. See URL routing. | | onRouteChange | (params: CalendarRouteParams) => void | — | Called whenever navigable state changes. The host app should write these params to the URL. See URL routing. |


URL routing / deep linking

By default the calendar is stateless from a URL perspective — navigating to a court date or applying a filter leaves no trace in the browser address bar. The routeParams / onRouteChange props opt into URL sync without coupling the library to any specific router.

How it works

The library never reads or writes window.location directly when onRouteChange is provided. Instead:

  • Outbound: whenever navigable state changes (view, date, open modal, active filters), onRouteChange is called with a CalendarRouteParams object describing the full current state.
  • Inbound: on mount, the library reads routeParams once and restores the saved state — including re-opening a court date modal or drilling straight into a case.

The host app owns the URL. It parses params out of its router, passes them as routeParams, and writes new params back in onRouteChange. Two utility functions handle the encoding:

import {
    CCalendar,
    parseCalendarSearchParams,
    serializeCalendarParams,
} from '@propriety/court-calendar';

Integration examples

React Router v7

import { useSearchParams } from 'react-router';
import { CCalendar, parseCalendarSearchParams, serializeCalendarParams } from '@propriety/court-calendar';

function CalendarPage() {
    const [searchParams, setSearchParams] = useSearchParams();

    return (
        <CCalendar
            apiKey={API_KEY}
            activeUser={userId}
            routeParams={parseCalendarSearchParams(searchParams)}
            onRouteChange={(params) =>
                setSearchParams(serializeCalendarParams(params), { replace: true })
            }
        />
    );
}

useSearchParams returns a standard URLSearchParams object which parseCalendarSearchParams accepts directly. { replace: true } keeps the history stack clean so back/forward skip filter changes.

React Router v6

import { useSearchParams } from 'react-router-dom';
// rest is identical to v7

React Router v6/v7 with useLocation

import { useLocation, useNavigate } from 'react-router';
import { CCalendar, parseCalendarSearchParams, serializeCalendarParams } from '@propriety/court-calendar';

function CalendarPage() {
    const location = useLocation();
    const navigate = useNavigate();

    return (
        <CCalendar
            apiKey={API_KEY}
            activeUser={userId}
            routeParams={parseCalendarSearchParams(new URLSearchParams(location.search))}
            onRouteChange={(params) => {
                const qs = serializeCalendarParams(params).toString();
                navigate(qs ? `${location.pathname}?${qs}` : location.pathname, { replace: true });
            }}
        />
    );
}

React Router v5

import { useLocation, useHistory } from 'react-router-dom';
import { CCalendar, parseCalendarSearchParams, serializeCalendarParams } from '@propriety/court-calendar';

function CalendarPage() {
    const location = useLocation();
    const history = useHistory();

    return (
        <CCalendar
            apiKey={API_KEY}
            activeUser={userId}
            routeParams={parseCalendarSearchParams(new URLSearchParams(location.search))}
            onRouteChange={(params) => {
                const qs = serializeCalendarParams(params).toString();
                history.replace(qs ? `${location.pathname}?${qs}` : location.pathname);
            }}
        />
    );
}

TanStack Router

import { useSearch, useNavigate } from '@tanstack/react-router';
import { CCalendar, parseCalendarSearchParams, serializeCalendarParams } from '@propriety/court-calendar';

function CalendarPage() {
    const search = useSearch({ from: '/calendar' });
    const navigate = useNavigate();

    return (
        <CCalendar
            apiKey={API_KEY}
            activeUser={userId}
            routeParams={parseCalendarSearchParams(search as Record<string, string>)}
            onRouteChange={(params) =>
                navigate({ search: Object.fromEntries(serializeCalendarParams(params)), replace: true })
            }
        />
    );
}

Next.js App Router

'use client';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { CCalendar, parseCalendarSearchParams, serializeCalendarParams } from '@propriety/court-calendar';

export default function CalendarPage() {
    const router = useRouter();
    const pathname = usePathname();
    const searchParams = useSearchParams();

    return (
        <CCalendar
            apiKey={API_KEY}
            activeUser={userId}
            routeParams={parseCalendarSearchParams(searchParams)}
            onRouteChange={(params) => {
                const qs = serializeCalendarParams(params).toString();
                router.replace(qs ? `${pathname}?${qs}` : pathname);
            }}
        />
    );
}

Standalone mode (no router)

When neither routeParams nor onRouteChange is provided, the library manages the URL itself using window.history.replaceState. This is suitable for single-page apps without a router or for standalone embedding. The URL updates in place without adding history entries.

URL parameter reference

Parameters use short keys to keep the address bar readable. Only non-default values appear in the URL — a fully default calendar state produces a clean URL with no search params.

| URL key | CalendarRouteParams field | Default (omitted when) | Notes | |---------|----------------------------|------------------------|-------| | view | view | 'dayGridMonth' | One of dayGridMonth, timeGridWeek, timeGridDay, listDay, tableView | | date | date | today | ISO YYYY-MM-DD | | cdId | courtDateId | absent | Integer court date ID — opens the details modal on load | | ci | caseIdx | absent | Case.SCARIndexNumber — opens a specific case (requires cdId) | | ht | showVirtual / showInPerson / showUnknown | all visible | Comma-separated list of hidden hearing types, e.g. ht=virtual,inPerson | | user | showOnlyAssignedToUser | null | UserID integer, or -1 for "Unassigned" | | dl | showUploadDeadlines | false | dl=1 when true | | adj | showAdjournmentDates | true | adj=0 when false | | dt | dateTypeFilter | 'all' | scar or negotiations | | muni | municode | null | Municipality code string | | settled | showOnlyUnsettled | true | settled=1 when the filter is off (inverted because default is on) | | noEvid | showOnlyWithoutEvidence | false | noEvid=1 when true | | unrev | showOnlyUnreviewed | false | unrev=1 when true | | q | searchTerm | '' | Free-text search |

Example URL after opening a case with a filter applied:

/calendar?cdId=1234&ci=2024-98765&view=listDay&date=2025-06-15&dt=scar

What is and isn't bookmarkable

The URL captures:

  • Current calendar view and visible date
  • Which court date modal is open (cdId)
  • Which case is open within that modal (ci)
  • All active filter state

The URL does not capture:

  • Edit mode or Create mode (transient form state — these are suppressed from onRouteChange)

Bootstrap sequence

When the page loads with params in the URL, the library restores state in phases to handle async data loading:

  1. Phase 1 (synchronous, on mount): View, date, and filter state are applied immediately from routeParams.
  2. Phase 1b: If user=N is in the URL, the User object is looked up from the reference data once it loads and merged into filter context. user=-1 (Unassigned) is handled as a special case — no lookup is needed.
  3. Phase 2: Once court date data finishes loading, the target court date is found and its details modal opened. If the court date has been filtered out of view (e.g. all its cases are settled and the unsettled filter is on), a minimal stub is built from the raw court date data so the modal still opens correctly.
  4. Phase 3: Once cases for the opened court date finish loading, the target case is found and opened. This works even for settled cases that are hidden from the calendar view — selectedCases always contains all cases regardless of active filters.

If a cdId or ci value in the URL doesn't match any loaded data, a brief error snackbar is shown.


Architecture

Components

| Component | Description | |-----------|-------------| | CCalendar | Root component. Manages state, events, filtering, and renders the calendar + modal. | | Toolbar | Navigation and filter controls (date type, hearing type, case filters, user filter, view switcher, municipality dropdown, search). | | CalendarList | MUI DataGrid table view of court dates with color-coded status, county, and location columns. | | Modal | Dialog with 4 modes: Details, Edit, Create, Case Details. | | CreateEditCase | Form for court date fields (dates, municipality, hearing details, chair assignments, case IDs). | | CaseViewer | DataGrid listing cases for a selected court date. | | CaseDetails | Read-only view of a single case with evidence and SCAR data. |

Hooks

| Hook | Description | |------|-------------| | useCourtDates | Fetches and caches court dates and negotiations. Handles polling and optimistic in-memory updates. | | useCaseData | Fetches and caches cases for all court dates. Maintains a Record<caseKey, Case[]> memory store. | | useCalendarEvents | Derives FullCalendar event objects and CalEvent buckets from court date + case data. Applies filter context. | | useModalSelection | Tracks which court date event is selected and manages openDetailsModal / openCreateModal. | | useModalState | Manages form state for Edit/Create modes and clickedCase for Case Details mode. | | useModalActions | Handles save, delete, and chair-update API calls. | | useRouteSync | Syncs navigable state to/from the URL. See URL routing. | | useScrollbarClickDetection | Prevents date-click from firing when the user clicks a scrollbar. |

Helpers

| Helper | Description | |--------|-------------| | helpers/routing.ts | parseCalendarSearchParams, serializeCalendarParams, buildCalendarRouteParams, extractFilterFields. Pure functions for URL encoding/decoding. | | helpers/CalEvent.ts | buildCalEvents — derives main, extra-day, and adjournment CalEvent buckets from court date + case data. | | helpers/cases.ts | Case fetching (by court date, paginated, search). Settlement and evidence checks. | | helpers/courtDates.ts | caseKey (memory-map key), isVillageDate (municipality code check). | | helpers/documentActions.ts | Open, download, and print document helpers (used by DocCard). | | helpers/formatter.ts | Date formatting for API (YYYY-MM-DD or YYYY-MM-DD HH:mm:00). Evidence string formatting. | | helpers/cache.ts | Dexie-based IndexedDB caching with TTL. | | helpers/api/ | Fetch wrappers for each API endpoint group (court dates, cases, munis, people). |

CalEvent

CalEvent is the internal representation of a single calendar entry. One CourtDate can produce multiple CalEvent objects:

| Type | isAdjourned | isExtraDay | Description | |------|---------------|--------------|-------------| | Main | false | false | The primary entry on the original court date. Holds all non-adjourned cases. | | Extra-day | false | true | Additional days from CourtDate.MultipleDates. Same cases as the main event. | | Adjournment | true | false | One entry per unique adjourned date, holding the cases adjourned to that date. |

URL routing always targets the main CalEvent when restoring a modal — adjournment and extra-day entries are never used as the bootstrap target.

Key types

interface CalendarRouteParams {
    view?: 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay' | 'listDay' | 'tableView';
    date?: string;                        // ISO: "YYYY-MM-DD"
    courtDateId?: number;
    caseIdx?: string;                     // Case.SCARIndexNumber
    showVirtual?: boolean;
    showInPerson?: boolean;
    showUnknown?: boolean;
    showOnlyAssignedToUser?: number | null;
    showUploadDeadlines?: boolean;
    showAdjournmentDates?: boolean;
    dateTypeFilter?: 'all' | 'scar' | 'negotiations';
    municode?: string | null;
    showOnlyUnsettled?: boolean;
    showOnlyWithoutEvidence?: boolean;
    showOnlyUnreviewed?: boolean;
    searchTerm?: string;
}

interface CourtDate {
    CourtDateID: number;
    CourtDate: Date;
    MuniCode: string;
    UploadDeadline: Date | null;
    CourtCases: number;
    Lifecycle: 'Scheduled' | 'Assigned' | 'Uploaded' | 'Adjourned' | null;
    DateType: '' | 'Negotiations' | null;  // '' = SCAR
    HearingTime: string;
    HearingLink: string | null;
    Source: 'manual' | 'email' | 'etrack' | null;
    Type: 'Other' | 'Virtual' | 'InPerson' | null;
    FirstChair: number | null;
    SecondChair: number | null;
    HearingOfficer: number | null;
    Notes: Notes[] | null;
    NoticeFile: string | null;
    MultipleDates: Date[] | null;
    Address: string | null;
    Room: string | null;
    SkipMotion: boolean | null;
}

interface Case {
    ParcelID: string;
    Municipality: string;
    SCARIndexNumber: string;
    SCARDeterminationAction: string;
    property_data: { Address: string; PropertyOwnerFull: string };
    evidence: Evidence | null;
    AdjournedDate: Date | null;
    // ... plus Village SCAR fields and negotiation/stip tracking
}

Development

Prerequisites

  • Node.js 20+
  • npm

Setup

git clone [email protected]:Aventine-Git/court-calendar.git
cd court-calendar
npm install

Dev server

npm run dev

Starts the Vite dev server using dev/App.tsx as the entry point. The API only accepts requests from localhost:8000 — the dev server must run on port 8000 or API calls will fail with CORS errors.

Environment variables

Create a .env file in the project root:

VITE_CALENDAR_API_KEY=your-api-key-here

Build

npm run build

Runs tsc -b then the Vite library build. Output in dist/:

  • dist/index.mjs — ES module bundle
  • dist/index.d.ts — TypeScript declarations
  • dist/court-calendar.css — compiled styles

React, React-DOM, MUI, and Emotion are externalized (peer dependencies, not bundled).

Lint

npm run lint

ESLint with TypeScript plugin. Biome is configured for formatting (4-space indent, 120-char line width, single quotes).


Testing

npm run test

Vitest + @testing-library/react + jsdom. Test files live in src/__tests__/ mirroring the source tree:

| Test file | Covers | |-----------|--------| | helpers/CalEvent.test.ts | buildCalEvents, parseLocalDate | | helpers/routing.test.ts | serializeCalendarParams, parseCalendarSearchParams, buildCalendarRouteParams, extractFilterFields | | hooks/UseModalState.test.ts | useModalState — edit/create init, modal close effects, clickedCase transitions | | hooks/UseRouteSync.test.ts | useRouteSync — outbound sync, replaceState fallback, bootstrap Phases 1–3, bootstrap errors |


API

All data is fetched from https://utils.aventine.ai. Every request requires the x-api-key header.

Endpoints

| Endpoint | Method | Description | |----------|--------|-------------| | /court-dates/all | GET | Fetch all court dates | | /court-dates/create | POST | Create a new court date | | /court-dates/{id}/update | PUT | Update a court date | | /court-dates/{id} | DELETE | Delete a court date | | /court-dates/{id}/cases | GET | Fetch cases for a court date | | /court-cases/search?term=...&page=... | GET | Search cases (paginated) | | /court-cases/filtering?page=...&page_size=... | GET | List all cases (paginated) | | /court-cases/snooze/upload/{id} | GET | Snooze upload deadline by 1 business day | | /users/all | GET | Fetch all users | | /users/hearing-officers | GET | Fetch hearing officers | | /utils/get-muni-names | GET | Fetch municipality names |


CI/CD

Publishing is automated via GitHub Actions (.github/workflows/publish.yml).

Trigger: every merged PR to main.

Steps:

  1. Checkout code
  2. Install dependencies (npm ci)
  3. Build (npm run build)
  4. Set version to 0.0.{run_number} (auto-incrementing, no commit needed)
  5. Publish to npm with provenance

Required secrets:

  • NPM_TOKEN — npm access token with publish rights for the @propriety scope

Known limitations

  • Port 8000 only: The backend API enforces CORS that only allows requests from localhost:8000. Applies to the dev server and any locally running consumer.
  • Cache is not invalidated on external changes: If another user modifies a court date, the change won't appear until the local cache expires (1 minute for dates, 2 minutes for cases).
  • No pagination for court dates: getAllDates fetches every court date in a single request.
  • Single API key: No built-in token refresh or rotation.
  • No error UI for API failures: Network errors are logged to the console but not surfaced to the user (except for bootstrap errors, which show a brief snackbar).