# 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`, 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 `` 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` and add an explanation comment, like so: ```tsx ``` - 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') ---

Edit service

({ 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} /> ({ label: category.name, value: category.id.toString(), icon: category.icon, }))} error={inputErrors.categories} />
``` - 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 = { 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 => ({ 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[] ); ```