308 lines
10 KiB
Plaintext
308 lines
10 KiB
Plaintext
# 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' }}
|
|
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}
|
|
rows={10}
|
|
error={inputErrors.notes}
|
|
/>
|
|
|
|
<Captcha action={actions.serviceSuggestion.createService} />
|
|
|
|
<InputHoneypotTrap name="message" />
|
|
|
|
<InputSubmitButton />
|
|
</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>[]
|
|
);
|
|
```
|