Release 2025-05-19
This commit is contained in:
307
.cursorrules
307
.cursorrules
@@ -1,307 +0,0 @@
|
||||
# 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>[]
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user