# Cursor Rules

- When merging tailwind classes, use the `cn` function.
- When using Tailwind and you need to merge classes use the `cn` function if avilable.
- We use Tailwind 4 (the latest version), make sure to not use outdated classes.
- Instead of using the syntax`Array<T>`, use `T[]`.
- Use TypeScript `type` over `interface`.
- You are forbiddent o add comments unless explicitly stated by the user.
- Avoid sending JavaScript to the client. The JS send should be optional.
- In prisma preffer `select` over `include` when making queries.
- Import the types from prisma instead of hardcoding duplicates.
- Avoid duplicating similar html code, and parametrize it when possible or create separate components.
- Remember to check the prisma schema when doing things related to the database.
- Avoid hardcoding enums from the database, import them from prisma.
- Avoid using client-side JavaScript as much as possible. And if it has to be done, make it optional.
- The admin pages can use client-side JavaScript.
- Keep README.md in sync with new capabilities.
- The package manager is npm.
- For icons use the `Icon` component from `astro-icon/components`.
- For icons use the Remix Icon library preferably.
- Use the `Image` component from `astro:assets` for images.
- Use the `zod` library for schema validation.
- In the astro actions return, don't return success: true, or similar, just return an object with the newly created/edited objects or nothing.
- When adding actions, don't create and export a new variable called actions. Notice that Astro already provides that variable from `import { actions } from 'astro:actions'`. So just add the new actions to the `server` variable in `web/src/actions/index.ts` and that's it.
- Don't forget that the astro files have three dashes (`---`) at the begining of the file and where the server js ends. I noticed that sometimes you forget them.
- The admin actions go into a separate folder.
- In Actro actions when throwing errors use ActionError.
- @deprecated Don't import this object, use {@link actions} instead, like: `import { actions } from 'astro:actions'`. Example:

  ```ts
  import { actions } from 'astro:actions'; /* CORRECT */
  import { server } from '~/actions'; /* WRONG!!!! DON'T DO THIS */
  import { adminAttributeActions } from '~/actions/admin/attribute.ts'; /* WRONG!!!! DON'T DO THIS */

  const result = Astro.getActionResult(actions.admin.attribute.create);
  ```

- Always use Astro actions instead of with API routes or `if (Astro.request.method === "POST")`.
- When adding clientside js do it with HTMX.
- When adding HTMX, the layout component BaseLayout accepts a prop htmx to load it in that page. No need to use a cdn.
- When redirecting to login use the `makeLoginUrl` function from web/src/lib/redirectUrls.ts and if the link is for an `<a>` tag, use the `data-astro-reload` attribute.

  ```ts
  function makeLoginUrl(
    currentUrl: URL,
    options: {
      redirect?: URL | string | null;
      error?: string | null;
      logout?: boolean;
      message?: string | null;
    } = {}
  );
  ```

- When adding client scripts remember to use the event `astro:page-load`, `querySelectorAll<Type>` and add an explanation comment, like so:

  ```tsx
  <script>
    ////////////////////////////////////////////////////////////
    // Optional script for __________.                        //
    // Desctiption goes here...                               //
    ////////////////////////////////////////////////////////////

    document.addEventListener('astro:page-load', () => {
      document.querySelectorAll<HTMLDivElement>('[data-my-div]').forEach((myDiv) => {
        // Do something
      })
    })
  </script>
  ```

- When creating forms, we already have utilities, components and established design patterns. Follow this example:

  ```astro
  ---
  import { actions, isInputError } from 'astro:actions'
  import { z } from 'astro:content'

  import Captcha from '../../components/Captcha.astro'
  import InputCardGroup from '../../components/InputCardGroup.astro'
  import InputCheckboxGroup from '../../components/InputCheckboxGroup.astro'
  import InputHoneypotTrap from '../../components/InputHoneypotTrap.astro'
  import InputImageFile from '../../components/InputImageFile.astro'
  import InputSubmitButton from '../../components/InputSubmitButton.astro'
  import InputText from '../../components/InputText.astro'
  import InputTextArea from '../../components/InputTextArea.astro'
  import { kycLevels } from '../../constants/kycLevels'
  import BaseLayout from '../../layouts/BaseLayout.astro'
  import { zodParseQueryParamsStoringErrors } from '../../lib/parseUrlFilters'
  import { prisma } from '../../lib/prisma'
  import { makeLoginUrl } from '../../lib/redirectUrls'

  const user = Astro.locals.user
  if (!user) {
    return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to suggest a new service' }))
  }

  const result = Astro.getActionResult(actions.serviceSuggestion.editService)
  if (result && !result.error) {
    return Astro.redirect(`/service-suggestion/${result.data.serviceSuggestion.id}`)
  }
  const inputErrors = isInputError(result?.error) ? result.error.fields : {}

  const { data: params } = zodParseQueryParamsStoringErrors(
    {
      serviceId: z.coerce.number().int().positive(),
      notes: z.string().default(''),
    },
    Astro
  )

  if (!params.serviceId) return Astro.rewrite('/404')

  const service = await Astro.locals.banners.try(
    'Failed to fetch service',
    async () =>
      prisma.service.findUnique({
        select: {
          id: true,
          name: true,
          slug: true,
          description: true,
          overallScore: true,
          kycLevel: true,
          imageUrl: true,
          verificationStatus: true,
          acceptedCurrencies: true,
          categories: {
            select: {
              name: true,
              icon: true,
            },
          },
        },
        where: { id: params.serviceId },
      }),
    null
  )

  if (!service) return Astro.rewrite('/404')
  ---

  <BaseLayout
    pageTitle="Edit service"
    description="Suggest an edit to service"
    ogImage={{
      template: 'generic',
      title: 'Edit service',
      description: 'Suggest an edit to service',
      icon: 'ri:edit-line',
    }}
    widthClassName="max-w-screen-md"
  >
    <h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Edit service</h1>

    <form method="POST" action={actions.serviceSuggestion.editService} class="space-y-6">
      <input type="hidden" name="serviceId" value={params.serviceId} />

      <InputText
        label="Service name"
        name="name"
        value={service.name}
        error={inputErrors.name}
        inputProps={{ 'data-custom-value': true, required: true }}
      />

      <InputCardGroup
        name="kycLevel"
        label="KYC Level"
        options={kycLevels.map((kycLevel) => ({
          label: kycLevel.name,
          value: kycLevel.id.toString(),
          icon: kycLevel.icon,
          description: `${kycLevel.description}\n\n_KYC Level ${kycLevel.value}/5_`,
        }))}
        iconSize="md"
        cardSize="md"
        required
        error={inputErrors.kycLevel}
      />

      <InputCheckboxGroup
        name="categories"
        label="Categories"
        required
        options={categories.map((category) => ({
          label: category.name,
          value: category.id.toString(),
          icon: category.icon,
        }))}
        error={inputErrors.categories}
      />

      <InputImageFile
        label="Service Image"
        name="imageFile"
        description="Square image. At least 192x192px. Transparency supported."
        error={inputErrors.imageFile}
        square
        required
      />

      <InputTextArea
        label="Note for Moderators"
        name="notes"
        value={params.notes}
        inputProps={{ rows: 10 }}
        error={inputErrors.notes}
      />

      <Captcha action={actions.serviceSuggestion.createService} />

      <InputHoneypotTrap name="message" />

      <InputSubmitButton hideCancel />
    </form>
  </BaseLayout>
  ```

- Don't use the `web/src/pages/admin` pages as example unless explicitly stated or you're creating/editing an admin page.
- When creating constants or enums, use the `makeHelpersForOptions` function like in this example. Save the file in the `web/src/constants` folder. Note that it's not necessary to use all the options the example has, just the ones you need.

```ts
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions';
import { transformCase } from '../lib/strings';

import type { AttributeType } from '@prisma/client';

type AttributeTypeInfo<T extends string | null | undefined = string> = {
  value: T;
  slug: string;
  label: string;
  icon: string;
  order: number;
  classNames: {
    text: string;
    icon: string;
  };
};

export const {
  dataArray: attributeTypes,
  dataObject: attributeTypesById,
  getFn: getAttributeTypeInfo,
  getFnSlug: getAttributeTypeInfoBySlug,
  zodEnumBySlug: attributeTypesZodEnumBySlug,
  zodEnumById: attributeTypesZodEnumById,
  keyToSlug: attributeTypeIdToSlug,
  slugToKey: attributeTypeSlugToId,
} = makeHelpersForOptions(
  'value',
  (value): AttributeTypeInfo<typeof value> => ({
    value,
    slug: value ? value.toLowerCase() : '',
    label: value
      ? transformCase(value.replace('_', ' '), 'title')
      : String(value),
    icon: 'ri:question-line',
    order: Infinity,
    classNames: {
      text: 'text-current/60',
      icon: 'text-current/60',
    },
  }),
  [
    {
      value: 'BAD',
      slug: 'bad',
      label: 'Bad',
      icon: 'ri:close-line',
      order: 1,
      classNames: {
        text: 'text-red-200',
        icon: 'text-red-400',
      },
    },
    {
      value: 'WARNING',
      slug: 'warning',
      label: 'Warning',
      icon: 'ri:alert-line',
      order: 2,
      classNames: {
        text: 'text-yellow-200',
        icon: 'text-yellow-400',
      },
    },
    {
      value: 'GOOD',
      slug: 'good',
      label: 'Good',
      icon: 'ri:check-line',
      order: 3,
      classNames: {
        text: 'text-green-200',
        icon: 'text-green-400',
      },
    },
    {
      value: 'INFO',
      slug: 'info',
      label: 'Info',
      icon: 'ri:information-line',
      order: 4,
      classNames: {
        text: 'text-blue-200',
        icon: 'text-blue-400',
      },
    },
  ] as const satisfies AttributeTypeInfo<AttributeType>[]
);
```
