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

@mrfylke/contact-form

v0.1.31

Published

A configurable contact form package for public transportation organizations.

Readme

@mrfylke/contact-form

A configurable contact form package for public transportation organizations.

Installation

npm install @mrfylke/contact-form

Peer dependencies

npm install react react-dom next @atb-as/theme @internationalized/date react-aria-components

Quick start

1. Create your configuration

import { ContactFormConfig } from '@mrfylke/contact-form';

export const contactFormConfig: ContactFormConfig = {
  theme: {
    light: { /* colors, spacer, border */ },
    dark:  { /* same structure */ },
  },
  api: {
    onSubmit,
  },
};

2. Wrap your app

import { ContactFormRoot } from '@mrfylke/contact-form';
import { contactFormConfig } from './config/contact-form-config';

export function RootLayout({ children }) {
  const acceptLanguage = (await headers()).get('accept-language');
  return (
    <ContactFormRoot config={contactFormConfig} acceptLanguageHeader={acceptLanguage ?? undefined}>
      {children}
    </ContactFormRoot>
  );
}

3. Render form pages

import { RefundContent, ContactPageLayout } from '@mrfylke/contact-form';

export default function ContactPage() {
  return (
    <ContactPageLayout title="Contact Us">
      <RefundContent />
    </ContactPageLayout>
  );
}

API configuration

The package is decoupled from your backend. By default, it POSTs to endpoints.submit (/api/contact/submit) with { ...state, $slug: slug } in the body — no onSubmit needed:

api: {
  endpoints: {
    submit: '/api/contact/submit', // Optional, this is the default
  },
}

Pair that with createSubmitRouteHandler for the route itself — it reads $slug from the body and forwards the rest to your connector at that path:

// app/api/contact/submit/route.ts
import { createSubmitRouteHandler } from '@mrfylke/contact-form/server';

export const POST = createSubmitRouteHandler({
  request: (path, init) => fetch(`${process.env.CONNECTOR_URL}${path}`, init),
});

Overriding the submit callback

For full control over routing, provide your own onSubmit callback instead — it receives the form slug and form state:

import type { FormSlug } from '@mrfylke/contact-form';

const onSubmit = async (
  slug: FormSlug,
  state: Record<string, unknown>,
): Promise<Response> => {
  return fetch('/api/contact/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...state, formType: slug }),
  });
};

See FormSlug for the full set of possible values.

Routing submissions to different endpoints

const onSubmit = async (slug: FormSlug, state) => {
  const endpointMap: Record<FormSlug, string> = {
    'ticket-control':    '/api/contact/ticket-control',
    refund:              '/api/contact/refund',
    'means-of-transport': '/api/contact/means-of-transport',
    ticketing:           '/api/contact/ticketing',
    'journey-info':      '/api/contact/journey-info',
    'lost-property':     '/api/contact/lost-property',
  };
  return fetch(endpointMap[slug], {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(state),
  });
};

Payload types

Import these in your API handlers to type-check incoming data:

import type {
  TicketControlSubmitPayload,
  RefundSubmitPayload,
  MeansOfTransportSubmitPayload,
  TicketingSubmitPayload,
  JourneyInfoSubmitPayload,
} from '@mrfylke/contact-form';

Attachments are base64-encoded strings within the payload.

Lines endpoint

The package includes a ready-made route handler for fetching transit lines from Entur.

Option 1 – ready-made handler (recommended)

// app/api/contact/lines/route.ts
import { createLinesRouteHandler } from '@mrfylke/contact-form/server';

export const GET = createLinesRouteHandler();
// Or with explicit authority:
// export const GET = createLinesRouteHandler({ getAuthorityId: () => 'ATB:Authority:2' });

Set ENTUR_AUTHORITY_ID in your .env and point api.endpoints.lines at the mounted path.

Option 2 – call getLines() directly

import { getLines } from '@mrfylke/contact-form/server';

export async function GET() {
  const lines = await getLines(); // reads ENTUR_AUTHORITY_ID from env
  return Response.json(lines);
}

Server exports: createLinesRouteHandler · getLines · getEnturAuthorityId · EnturAuthority

ContactFormRoot props

| Prop | Type | Description | |---|---|---| | config | ContactFormConfig | Required. Theme, icons, API, org, features. | | acceptLanguageHeader | string | Raw Accept-Language header from the server. | | initialLanguage | Language | Client-side initial language when no header is available. | | forceTheme | 'light' \| 'dark' | Forces a theme variant; disables the dark-mode toggle. | | translationsOverride | ContactFormTranslationsOverride | Merges additional translation overrides on top of config.translations. |

Container and layout

import { ContactFormRoot, ContactFormContainer, ContactPageLayout } from '@mrfylke/contact-form';

<ContactFormRoot config={contactFormConfig}>
  <ContactFormContainer header={<MyHeader />} footer={<MyFooter />}>
    <ContactPageLayout title="Contact" backLink={backLink}>
      {content}
    </ContactPageLayout>
  </ContactFormContainer>
</ContactFormRoot>

Header and footer are your app's responsibility. Their components can use useContactFormConfig(), useTheme(), and useTranslation() from the package.

Layout config

layout: {
  basePath: '/contact',
  backLinkDefault: { href: '/', label: PageText.Contact.contactPageLayout.homeLink },
  successPath: '/contact/thanks',   // default: basePath + '/success'
  errorPath:   '/contact/oops',     // default: basePath + '/error'
  successContent: <CustomThanksMessage />,
  errorContent:   <CustomErrorMessage />,
  footerUrls: {
    privacy:       'https://example.com/privacy',
    accessibility: 'https://example.com/accessibility',
    help:          '/contact',
    tickets:       'https://example.com/tickets',
    homePage:      { href: 'https://example.com', name: 'example.com' },
  },
}

Render SuccessContent/ErrorContent (or your overrides) without ContactPageLayout — they are standalone result pages.

Deep-linking

Form type selections are reflected in the URL as a ?type= query parameter. This allows direct links to specific form states.

Reading on load: on mount each form section reads ?type= and selects the matching radio if it is in the enabled set.

Writing on change: selecting a radio updates ?type= via replaceState (no page reload).

Bypassing agreement checkboxes: for sections with an initial agreement step (refund), append &agreed=true to pre-tick the checkbox and jump straight to the form.

Available values per section:

| Section | ?type= values | |---|---| | Means of transport | driver · transportation · delay · stop · serviceOffering · injury | | Ticket control | feeComplaint · feedback · postponePayment | | Refund | refundOfTicket · refundAndTravelGuarantee · residualValueOnTravelCard | | Ticketing | priceAndTicketTypes · app · webshop · travelCard |

Examples:

  • /contact/refund?type=refundAndTravelGuarantee&agreed=true — opens travel guarantee form with agreement pre-accepted
  • /contact/ticket-control?type=feeComplaint — opens fee complaint form directly

Only values present in formSchemaOverrides.enabledFormTypes / enabledFormCategories are accepted; invalid values are ignored.

Translations

Translations are required — the package ships no built-in copy. The key structure follows planner-web as source of truth.

import {
  PageText,
  ComponentText,
  CommonText,
  ServerText,
} from '@mrfylke/contact-form/translations';

const contactFormConfig: ContactFormConfig = {
  translations: { pages: PageText, components: ComponentText, common: CommonText, server: ServerText },
};

To override specific keys, merge with the defaults:

const customTranslations = {
  pages: {
    Contact: {
      contactPageLayout: {
        title: { no: 'Kontakt oss', en: 'Contact us', nn: 'Kontakt oss' },
      },
    },
  },
  components: ComponentText,
  common: CommonText,
  server: ServerText,
};

Theme

Using @atb-as/theme (recommended)

import { createThemesFor, ThemeVariant } from '@atb-as/theme';
import { adaptAtbTheme } from '@mrfylke/contact-form/config';

const theme = adaptAtbTheme(createThemesFor(ThemeVariant.FRAM));

Manual theme

type Theme = {
  light: ThemeColors;
  dark:  ThemeColors;
};

type ThemeColors = {
  color: {
    transport:   TransportColors;
    status:      StatusColors;
    background:  BackgroundColors;
    interactive: { primary: InteractiveColor; secondary: InteractiveColor; destructive: InteractiveColor };
  };
  spacer: { small: string; medium: string; large: string; xLarge: string };
  border: { radius: { small: string; regular: string; circle: string }; width: { slim: string; medium: string } };
};

Use themeOverrides for small tweaks without rebuilding the full theme:

themeOverrides: { light: { spacer: { medium: '1.25rem' } } }

Icon set

Provide a React component for each key. Each component accepts size, className, role, alt, and style props.

Transportation: transportation/BusFill · transportation/TrainFill · transportation/TramFill · transportation/WalkFill · transportation/BicycleFill · transportation/PlaneFill · transportation/BoatFill · transportation/FerryFill · transportation/MetroFill · transportation/UnknownFill

Actions: actions/Close · actions/Clear · actions/Add

Status: status/CheckmarkFill · status/ErrorFill

Overriding standard behavior

| Feature | Default | Config key | |---|---|---| | Pages/tabs | All 7 tabs, all internal | pagesOverrides | | Ferry transport mode | Included | features.includeFerry | | Form schemas | All types/categories per page | formSchemaOverrides | | Header links | Single optional home link | layout.headerLinksOverride | | Footer links | Standard sections | layout.footerLinksOverride | | Translations | No defaults — must be provided | translations | | Theme | Must be provided | theme + themeOverrides |

Pages

pagesOverrides: {
  hidePageIds: ['lost-property'],
  externalUrlByPageId: { refund: 'https://example.com/refund-form' },
}

Page IDs: 'ticket-control' · 'refund' · 'means-of-transport' · 'ticketing' · 'lost-property' · 'group-travel' · 'journey-info'

Ferry flag

features: { includeFerry: false } // removes ferry from transport mode dropdowns and line queries

Form schema overrides

formSchemaOverrides: {
  ticketControl:    { enabledFormTypes:       ['feeComplaint', 'feedback'] },
  meansOfTransport: { enabledFormTypes:       ['driver', 'delay', 'transportation'] },
  refund:           { enabledFormCategories:  ['refundOfTicket', 'refundAndTravelGuarantee'] },
  ticketing:        { enabledFormCategories:  ['app', 'webshop'] },
}

Can also be loaded from a JSON/YAML file:

import schemaOverrides from './config/contact-form-schema.json';
// ...
formSchemaOverrides: schemaOverrides,

Header links override

layout: {
  headerLinksOverride: [
    { href: 'https://example.com', label: { no: 'Hjem', en: 'Home', nn: 'Heim' } },
  ],
}

Empty array removes all header links.

Footer links override

layout: {
  footerLinksOverride: [
    { href: 'https://example.com/tickets', label: { no: 'Billetter', en: 'Tickets', nn: 'Billettar' }, section: 'general' },
    { href: 'https://example.com/privacy', label: { no: 'Personvern', en: 'Privacy', nn: 'Personvern' }, section: 'bottom' },
  ],
}

Links with section: 'bottom' replace the standard privacy/accessibility row.

Styling

import '@mrfylke/contact-form/lib/styles.css';

Examples

See the apps/next-demo directory for a complete implementation, including commented examples of all override options in apps/next-demo/src/config/contact-form-config.tsx.

Troubleshooting

useContactFormConfig must be used within ContactFormRoot — wrap your app with <ContactFormRoot>.

Icons not showing — verify all required icon keys are provided and check the browser console for warnings.

Theme not applying — ensure <ContactFormRoot> wraps everything and your theme object matches the expected structure.

License

EUPL-1.2