From ac9a2f428a663ddfc46ef7ce005a3e3213949ca7 Mon Sep 17 00:00:00 2001 From: pluja Date: Sun, 25 May 2025 10:07:02 +0000 Subject: [PATCH] Release 2025-05-25-ELtG --- .cursor/rules/astro-actions-api.mdc | 20 ++ .cursor/rules/client-side-javascript.mdc | 26 ++ .cursor/rules/code-style.mdc | 8 + .cursor/rules/database.mdc | 54 +++ .cursor/rules/design-patterns.mdc | 98 ++++++ .cursor/rules/pages-and-components.mdc | 161 +++++++++ .cursor/rules/styles.mdc | 10 + .cursorrules | 312 ------------------ .gitignore | 3 +- docker-compose.yml | 2 + web/src/components/Button.astro | 33 +- web/src/components/Footer.astro | 2 +- web/src/components/ServicesFilters.astro | 4 +- .../components/ServicesSearchResults.astro | 31 +- web/src/lib/defineProtectedAction.ts | 2 +- web/src/pages/about.mdx | 1 - web/src/pages/admin/announcements/index.astro | 179 +++++----- web/src/pages/admin/attributes.astro | 118 ++++--- .../admin/service-suggestions/[id].astro | 44 ++- .../admin/service-suggestions/index.astro | 35 +- .../pages/admin/services/[slug]/edit.astro | 48 +-- web/src/pages/admin/services/index.astro | 69 ++-- web/src/pages/admin/users/index.astro | 73 ++-- web/src/pages/index.astro | 7 +- 24 files changed, 776 insertions(+), 564 deletions(-) create mode 100644 .cursor/rules/astro-actions-api.mdc create mode 100644 .cursor/rules/client-side-javascript.mdc create mode 100644 .cursor/rules/code-style.mdc create mode 100644 .cursor/rules/database.mdc create mode 100644 .cursor/rules/design-patterns.mdc create mode 100644 .cursor/rules/pages-and-components.mdc create mode 100644 .cursor/rules/styles.mdc delete mode 100644 .cursorrules diff --git a/.cursor/rules/astro-actions-api.mdc b/.cursor/rules/astro-actions-api.mdc new file mode 100644 index 0000000..6287046 --- /dev/null +++ b/.cursor/rules/astro-actions-api.mdc @@ -0,0 +1,20 @@ +--- +description: +globs: web/src/actions,web/src/pages +alwaysApply: false +--- +- In the astro actions return, generaly don't return anythig unless the caller doesn't needs it. Specially don't `return { success: true }`, or similar. If needed, just return an object with the newly created/edited objects (Like: `return { newService: service }` or don't return anything if not needed). +- When importing actions, use `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); + ``` +- 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. +- When throwing errors in Astro actions use ActionError. +- Always use Astro actions instead of with API routes and instead of `if (Astro.request.method === "POST")`. +- Generally call the actions using html forms. But if you need to, you can call them from the server-side code with Astro.callAction(), or [callActionWithUrlParams.ts](mdc:web/src/lib/callActionWithUrlParams.ts). +- The admin actions go into a separate folder. \ No newline at end of file diff --git a/.cursor/rules/client-side-javascript.mdc b/.cursor/rules/client-side-javascript.mdc new file mode 100644 index 0000000..623b462 --- /dev/null +++ b/.cursor/rules/client-side-javascript.mdc @@ -0,0 +1,26 @@ +--- +description: +globs: web/src/pages,web/src/components +alwaysApply: false +--- +- Avoid sending JavaScript to the client. The JS send should always be optional. +- Avoid using client-side JavaScript as much as possible. And if it has to be done, make it optional. +- To avoid using JavaScript, you can use HTML and CSS features such as hidden checkboxes, deltails tag, etc. +- The admin pages can use client-side JavaScript. +- When adding clientside JS do it with HTMX. +- When adding HTMX, the layout component BaseLayout [BaseLayout.astro](mdc:web/src/layouts/BaseLayout.astro) [BaseHead.astro](mdc:web/src/components/BaseHead.astro) accepts a prop htmx to load it in that page. No need to use a cdn. +- When adding client scripts remember to use the event `astro:page-load`, `querySelectorAll` and add an explanation comment, like so: + + ```tsx + + ``` diff --git a/.cursor/rules/code-style.mdc b/.cursor/rules/code-style.mdc new file mode 100644 index 0000000..8881364 --- /dev/null +++ b/.cursor/rules/code-style.mdc @@ -0,0 +1,8 @@ +--- +description: +globs: +alwaysApply: true +--- +- Instead of using the syntax`Array`, use `T[]`. +- Use TypeScript `type` over `interface`. +- You should never add unnecessary or unuseful comments, if you add a comment it must provide some value to the code. \ No newline at end of file diff --git a/.cursor/rules/database.mdc b/.cursor/rules/database.mdc new file mode 100644 index 0000000..a9bda76 --- /dev/null +++ b/.cursor/rules/database.mdc @@ -0,0 +1,54 @@ +--- +description: Querying the database, editing the database, needing to import types from the database, or anything related to the database or Prisma. +globs: +alwaysApply: false +--- +- We use Prisma as ORM. +- Remember to check the prisma schema [schema.prisma](mdc:web/prisma/schema.prisma) when doing things related to the database. +- Import the types from prisma instead of hardcoding duplicates. Specially use the Prisma.___GetPayload type and the enums. Like this: + ```ts + type Props = { + user: Prisma.UserGetPayload<{ + select: { + name: true + displayName: true + picture: true + } + }> + } + ``` + +- Only `select` the necessary fields, no more. +- In prisma preffer `select` over `include` when making queries. +- Avoid hardcoding enums from the database, import them from prisma. +- To query the database from Astro pages, use Astro.locals.try() or Astro.locals.tryMany([]) [errorBanners.ts](mdc:web/src/lib/errorBanners.ts) [middleware.ts](mdc:web/src/middleware.ts) , like so: +```ts +const [user, services] = await Astro.locals.banners.tryMany([ + [ + 'Error fetching user', + () => + prisma.user.findUnique({ + where: { id: userId }, + select: { + name: true, + displayName: true, + picture: true, + }, + }), + ], + [ + 'Error fetching services', + () => + prisma.service.findMany({ + where: { categories: { some: { id: categoryId } } }, + select: { + id: true, + name: true, + slug: true, + }, + }), + [] as [], + ], +]) +``` +- When editing the database, remember to edit the db seeding file [faker.ts](mdc:web/scripts/faker.ts) to generate data for the new schema. diff --git a/.cursor/rules/design-patterns.mdc b/.cursor/rules/design-patterns.mdc new file mode 100644 index 0000000..7723bca --- /dev/null +++ b/.cursor/rules/design-patterns.mdc @@ -0,0 +1,98 @@ +--- +description: +globs: +alwaysApply: true +--- +- The main libraries used are: Astro, TypeScript, Tailwind 4, HTMX, Prisma, npm, zod, lodash-es, date-fns, ts-toolbelt. Full list in: [package.json](mdc:web/package.json) +- When creating constants or enums, use the `makeHelpersForOptions` function [makeHelpersForOptions.ts](mdc:web/src/lib/makeHelpersForOptions.ts) like in this example. Save the file in the `web/src/constants` folder (like [attributeTypes.ts](mdc:web/src/constants/attributeTypes.ts)). Note that it's not necessary to use all the options or export all the variables that 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[] +); +``` diff --git a/.cursor/rules/pages-and-components.mdc b/.cursor/rules/pages-and-components.mdc new file mode 100644 index 0000000..6b34fa0 --- /dev/null +++ b/.cursor/rules/pages-and-components.mdc @@ -0,0 +1,161 @@ +--- +description: +globs: web/src/pages,web/src/components +alwaysApply: false +--- +- On .astro files, don't forget to include the three dashes (`---`) at the begining of the file and where the server js ends. I noticed that sometimes you forget them. +- For icons use the `Icon` component from `astro-icon/components`. +- For icons use the Remix Icon library preferably. +- Use the `MyPicture` component from `src/components/MyPicture.astro` for images. +- When redirecting to login use the `makeLoginUrl` function from [redirectUrls.ts](mdc:web/src/lib/redirectUrls.ts) and if the link is for an `` tag, use the `data-astro-reload` attribute. Similar for the logout and impersonate. +- Don't use the `web/src/pages/admin` pages as example unless explicitly stated or you're creating/editing an admin page. +- Checkout the @errorBanners.ts @middleware.ts @env.d.ts to see the avilable Astro.locals values. +- Avoid duplicating similar html code. You can use jsx for loops, create variables in the constants folder, or create separate components. +- When redirecting to the 404 not found page, use `Astro.rewrite` (Like this example: `if (!user) return Astro.rewrite('/404')`) +- Include schema markup in the pages when it makes sense. Examples: [[slug].astro](mdc:web/src/pages/service/[slug].astro) +- When creating forms, we already have utilities, components and established design patterns. Follow this example. (Note that this example may come slightly outdaded, but the overall philosophy doesn't change) + ```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} + /> + + + + + + + + + + + +
+ ``` diff --git a/.cursor/rules/styles.mdc b/.cursor/rules/styles.mdc new file mode 100644 index 0000000..b8692d9 --- /dev/null +++ b/.cursor/rules/styles.mdc @@ -0,0 +1,10 @@ +--- +description: +globs: /web/src/pages,/web/src/components,/web/src/constants +alwaysApply: false +--- +- We use Tailwind 4 (the latest version), make sure to not use outdated classes from Tailwind 3. +- Checkout the custom tailwind theme [global.css](mdc:web/src/styles/global.css). +- When adding conditional styles or merging tailwind classes, use the `cn` function. Never use template strings. [cn.ts](mdc:web/src/lib/cn.ts) +- For the grayscale colors, try to use the custom color `day` for the light/foreground colors (50-500) and `night` for the dark/bakground (500-950). +- Generally avoid using opacity modifiers (In `text-red-500/50` the `/50`), but it's fine to also use it. diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index b9a80a0..0000000 --- a/.cursorrules +++ /dev/null @@ -1,312 +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`, 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[] -); -``` diff --git a/.gitignore b/.gitignore index cb31a8c..fe09291 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ dump*.sql *.log *.bak migrate.py -sync-all.sh \ No newline at end of file +sync-all.sh +.DS_Store diff --git a/docker-compose.yml b/docker-compose.yml index f9b7310..fbd8ddc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,8 @@ services: POSTGRES_DB: ${POSTGRES_DATABASE:-kycnot} DATABASE_URL: "postgresql://${POSTGRES_USER:-kycnot}:${POSTGRES_PASSWORD:-kycnot}@database:5432/${POSTGRES_DATABASE:-kycnot}?schema=public" REDIS_URL: "redis://redis:6379" + env_file: + - .env depends_on: database: condition: service_healthy diff --git a/web/src/components/Button.astro b/web/src/components/Button.astro index f3ad4d1..caafc26 100644 --- a/web/src/components/Button.astro +++ b/web/src/components/Button.astro @@ -2,13 +2,15 @@ import { Icon } from 'astro-icon/components' import { tv, type VariantProps } from 'tailwind-variants' +import { cn } from '../lib/cn' + import type { HTMLAttributes, Polymorphic } from 'astro/types' type Props = Polymorphic< Required, Tag extends 'label' ? 'for' : never>> & VariantProps & { as: Tag - label?: string + label: string icon?: string endIcon?: string classNames?: { @@ -55,6 +57,7 @@ const button = tv({ iconOnly: { true: { base: 'p-0', + label: 'sr-only', }, }, color: { @@ -137,49 +140,49 @@ const button = tv({ color: 'black', variant: 'faded', class: { - base: 'border-night-300/30 bg-night-800/30 hover:bg-night-700/50 text-white/70 hover:text-white/90 focus-visible:ring-white/50', + base: 'bg2night-800/15 hover:bg-night-700/30 border-current/30 text-white/70 hover:text-white/90 focus-visible:ring-white/50', }, }, { color: 'white', variant: 'faded', class: { - base: 'border-day-300/30 bg-day-100/30 hover:bg-day-200/50 text-white/70 hover:text-white/90 focus-visible:ring-white/50', + base: 'b2-day-100/15 hover:bg-day-200/30 border-current/30 text-white/70 hover:text-white/90 focus-visible:ring-white/50', }, }, { color: 'gray', variant: 'faded', class: { - base: 'border-day-500/30 bg-day-400/30 hover:bg-day-500/50 text-day-300 hover:text-day-100 focus-visible:ring-white/50', + base: 'b2-day-400/15 hover:bg-day-500/30 text-day-300 hover:text-day-100 border-current/30 focus-visible:ring-white/50', }, }, { color: 'success', variant: 'faded', class: { - base: 'border-green-600/30 bg-green-500/30 text-green-300 hover:bg-green-500/50 hover:text-green-100', + base: 'border-current/20 bg-green-500/15 text-green-300 hover:bg-green-500/30 hover:text-green-100', }, }, { color: 'danger', variant: 'faded', class: { - base: 'border-red-600/30 bg-red-500/30 text-red-300 hover:bg-red-500/50 hover:text-red-100', + base: 'border-current/20 bg-red-500/15 text-red-300 hover:bg-red-500/30 hover:text-red-100', }, }, { color: 'warning', variant: 'faded', class: { - base: 'border-yellow-600/30 bg-yellow-500/30 text-yellow-300 hover:bg-yellow-500/50 hover:text-yellow-100', + base: 'border-current/20 bg-yellow-500/15 text-yellow-300 hover:bg-yellow-500/30 hover:text-yellow-100', }, }, { color: 'info', variant: 'faded', class: { - base: 'border-blue-600/30 bg-blue-500/30 text-blue-300 hover:bg-blue-500/50 hover:text-blue-100', + base: 'border-current/20 bg-blue-500/15 text-blue-300 hover:bg-blue-500/30 hover:text-blue-100', }, }, // Shadow variants @@ -260,6 +263,7 @@ const { dataAstroReload, disabled, inlineIcon, + iconOnly, ...htmlProps } = Astro.props @@ -268,13 +272,20 @@ const { icon: iconSlot, label: labelSlot, endIcon: endIconSlot, -} = button({ size, color, variant, shadow, disabled, iconOnly: !label && !(!!icon && !!endIcon) }) +} = button({ + size, + color, + variant, + shadow, + disabled, + iconOnly: iconOnly ?? (!label && !(!!icon && !!endIcon)), +}) const ActualTag = disabled && Tag === 'a' ? 'span' : Tag --- {!!icon && } - {!!label && {label}} + {label} { !!endIcon && ( diff --git a/web/src/components/Footer.astro b/web/src/components/Footer.astro index 3161448..37a4eda 100644 --- a/web/src/components/Footer.astro +++ b/web/src/components/Footer.astro @@ -52,7 +52,7 @@ const { class: className, ...htmlProps } = Astro.props href={href} target={external ? '_blank' : undefined} rel={external ? 'noopener noreferrer' : undefined} - class="text-day-500 dark:text-day-400 dark:hover:text-day-300 flex items-center gap-1 text-sm transition-colors hover:text-gray-700" + class="text-day-500 flex items-center gap-1 text-sm transition-colors hover:text-gray-200 hover:underline" > {label} diff --git a/web/src/components/ServicesFilters.astro b/web/src/components/ServicesFilters.astro index 6e6cea5..0e1992f 100644 --- a/web/src/components/ServicesFilters.astro +++ b/web/src/components/ServicesFilters.astro @@ -40,6 +40,7 @@ const { hx-select={`#${searchResultsId}`} hx-push-url="true" hx-indicator="#search-indicator" + hx-swap="outerHTML" data-services-filters-form data-default-verification-filter={options.verification .filter((verification) => verification.default) @@ -107,7 +108,7 @@ const { { options.categories?.map((category) => (
  • -
    - - - Back - + color="success" + variant="faded" + size="md" + icon="ri:arrow-left-s-line" + label="Back" + />

    Service Suggestion

    @@ -170,17 +176,23 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status) name="status" class="font-title w-full rounded-md border border-green-500/30 bg-black/50 p-2 text-sm text-gray-300 placeholder-gray-500 focus:border-green-500 focus:ring-green-500 disabled:opacity-50" > - - - - + { + serviceSuggestionStatuses.map((status) => ( + + )) + } - + color="success" + variant="faded" + size="md" + icon="ri:save-line" + label="Update" + /> diff --git a/web/src/pages/admin/service-suggestions/index.astro b/web/src/pages/admin/service-suggestions/index.astro index a09563d..ba46e16 100644 --- a/web/src/pages/admin/service-suggestions/index.astro +++ b/web/src/pages/admin/service-suggestions/index.astro @@ -4,8 +4,10 @@ import { actions } from 'astro:actions' import { z } from 'astro:content' import { orderBy } from 'lodash-es' +import Button from '../../../components/Button.astro' import SortArrowIcon from '../../../components/SortArrowIcon.astro' import TimeFormatted from '../../../components/TimeFormatted.astro' +import Tooltip from '../../../components/Tooltip.astro' import UserBadge from '../../../components/UserBadge.astro' import { getServiceSuggestionStatusInfo, @@ -183,12 +185,16 @@ const makeSortUrl = (slug: string) => {
    - + color="info" + variant="solid" + size="md" + iconOnly + icon="ri:search-2-line" + label="Search" + />
    @@ -317,13 +323,18 @@ const makeSortUrl = (slug: string) => {
    - - - + +
    diff --git a/web/src/pages/admin/services/[slug]/edit.astro b/web/src/pages/admin/services/[slug]/edit.astro index 521254b..b64a98f 100644 --- a/web/src/pages/admin/services/[slug]/edit.astro +++ b/web/src/pages/admin/services/[slug]/edit.astro @@ -27,46 +27,54 @@ import { eventTypes, getEventTypeInfo } from '../../../../constants/eventTypes' import { kycLevels } from '../../../../constants/kycLevels' import { serviceVisibilities } from '../../../../constants/serviceVisibility' import { verificationStatuses } from '../../../../constants/verificationStatus' -import { getVerificationStepStatusInfo } from '../../../../constants/verificationStepStatus' +import { + getVerificationStepStatusInfo, + verificationStepStatuses, +} from '../../../../constants/verificationStepStatus' import BaseLayout from '../../../../layouts/BaseLayout.astro' import { pluralize } from '../../../../lib/pluralize' import { prisma } from '../../../../lib/prisma' const { slug } = Astro.params -const serviceResult = Astro.getActionResult(actions.admin.service.update) -const eventCreateResult = Astro.getActionResult(actions.admin.event.create) -const eventToggleResult = Astro.getActionResult(actions.admin.event.toggle) -const eventDeleteResult = Astro.getActionResult(actions.admin.event.delete) -const eventUpdateResult = Astro.getActionResult(actions.admin.event.update) -const verificationStepCreateResult = Astro.getActionResult(actions.admin.verificationStep.create) -const verificationStepUpdateResult = Astro.getActionResult(actions.admin.verificationStep.update) -const verificationStepDeleteResult = Astro.getActionResult(actions.admin.verificationStep.delete) +if (!slug) return Astro.rewrite('/404') +const serviceResult = Astro.getActionResult(actions.admin.service.update) Astro.locals.banners.addIfSuccess(serviceResult, 'Service updated successfully') -Astro.locals.banners.addIfSuccess(eventCreateResult, 'Event created successfully') -Astro.locals.banners.addIfSuccess(eventToggleResult, 'Event visibility updated successfully') -Astro.locals.banners.addIfSuccess(eventDeleteResult, 'Event deleted successfully') -Astro.locals.banners.addIfSuccess(eventUpdateResult, 'Event updated successfully') -Astro.locals.banners.addIfSuccess(verificationStepCreateResult, 'Verification step added successfully') -Astro.locals.banners.addIfSuccess(verificationStepUpdateResult, 'Verification step updated successfully') -Astro.locals.banners.addIfSuccess(verificationStepDeleteResult, 'Verification step deleted successfully') +const serviceInputErrors = isInputError(serviceResult?.error) ? serviceResult.error.fields : {} if (serviceResult && !serviceResult.error && slug !== serviceResult.data.service.slug) { return Astro.redirect(`/admin/services/${serviceResult.data.service.slug}/edit`) } -const serviceInputErrors = isInputError(serviceResult?.error) ? serviceResult.error.fields : {} +const eventCreateResult = Astro.getActionResult(actions.admin.event.create) +Astro.locals.banners.addIfSuccess(eventCreateResult, 'Event created successfully') const eventInputErrors = isInputError(eventCreateResult?.error) ? eventCreateResult.error.fields : {} + +const eventUpdateResult = Astro.getActionResult(actions.admin.event.update) +Astro.locals.banners.addIfSuccess(eventUpdateResult, 'Event updated successfully') const eventUpdateInputErrors = isInputError(eventUpdateResult?.error) ? eventUpdateResult.error.fields : {} + +const eventToggleResult = Astro.getActionResult(actions.admin.event.toggle) +Astro.locals.banners.addIfSuccess(eventToggleResult, 'Event visibility updated successfully') + +const eventDeleteResult = Astro.getActionResult(actions.admin.event.delete) +Astro.locals.banners.addIfSuccess(eventDeleteResult, 'Event deleted successfully') + +const verificationStepCreateResult = Astro.getActionResult(actions.admin.verificationStep.create) +Astro.locals.banners.addIfSuccess(verificationStepCreateResult, 'Verification step added successfully') const verificationStepInputErrors = isInputError(verificationStepCreateResult?.error) ? verificationStepCreateResult.error.fields : {} + +const verificationStepUpdateResult = Astro.getActionResult(actions.admin.verificationStep.update) +Astro.locals.banners.addIfSuccess(verificationStepUpdateResult, 'Verification step updated successfully') const verificationStepUpdateInputErrors = isInputError(verificationStepUpdateResult?.error) ? verificationStepUpdateResult.error.fields : {} -if (!slug) return Astro.rewrite('/404') +const verificationStepDeleteResult = Astro.getActionResult(actions.admin.verificationStep.delete) +Astro.locals.banners.addIfSuccess(verificationStepDeleteResult, 'Verification step deleted successfully') const [service, categories, attributes] = await Astro.locals.banners.tryMany([ [ @@ -715,7 +723,7 @@ if (!service) return Astro.rewrite('/404') ({ + options={verificationStepStatuses.map((status) => ({ label: status.label, value: status.value, }))} @@ -763,7 +771,7 @@ if (!service) return Astro.rewrite('/404') ({ + options={verificationStepStatuses.map((status) => ({ label: status.label, value: status.value, }))} diff --git a/web/src/pages/admin/services/index.astro b/web/src/pages/admin/services/index.astro index 6abbc1e..2b5a8b4 100644 --- a/web/src/pages/admin/services/index.astro +++ b/web/src/pages/admin/services/index.astro @@ -3,8 +3,10 @@ import { ServiceVisibility, VerificationStatus, type Prisma } from '@prisma/clie import { z } from 'astro/zod' import { Icon } from 'astro-icon/components' +import Button from '../../../components/Button.astro' import MyPicture from '../../../components/MyPicture.astro' import SortArrowIcon from '../../../components/SortArrowIcon.astro' +import Tooltip from '../../../components/Tooltip.astro' import { getKycLevelInfo } from '../../../constants/kycLevels' import { getVerificationStatusInfo } from '../../../constants/verificationStatus' import BaseLayout from '../../../layouts/BaseLayout.astro' @@ -171,13 +173,15 @@ const truncate = (text: string, length: number) => {

    Service Management

    {totalServicesCount} services - - - New Service - + color="success" + variant="solid" + size="md" + icon="ri:add-line" + label="New Service" + />
    @@ -250,12 +254,17 @@ const truncate = (text: string, length: number) => { )) } - + color="info" + variant="solid" + size="md" + iconOnly + icon="ri:search-2-line" + label="Search" + class="ml-2" + /> @@ -437,20 +446,30 @@ const truncate = (text: string, length: number) => { {service.formattedDate}
    - - - - - Edit - + +
    diff --git a/web/src/pages/admin/users/index.astro b/web/src/pages/admin/users/index.astro index 197487b..eda4e3b 100644 --- a/web/src/pages/admin/users/index.astro +++ b/web/src/pages/admin/users/index.astro @@ -3,6 +3,7 @@ import { Icon } from 'astro-icon/components' import { z } from 'astro:content' import { orderBy as lodashOrderBy } from 'lodash-es' +import Button from '../../../components/Button.astro' import SortArrowIcon from '../../../components/SortArrowIcon.astro' import TimeFormatted from '../../../components/TimeFormatted.astro' import Tooltip from '../../../components/Tooltip.astro' @@ -151,12 +152,17 @@ const makeSortUrl = (sortBy: NonNullable<(typeof filters)['sort-by']>) => { - + color="info" + variant="solid" + size="md" + iconOnly + icon="ri:search-2-line" + label="Search" + class="rounded-l-none" + /> @@ -320,30 +326,43 @@ const makeSortUrl = (sortBy: NonNullable<(typeof filters)['sort-by']>) => {
    - - + +
    diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index fdfcc72..cedf43b 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -322,7 +322,11 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] = icon: true, _count: { select: { - services: true, + services: { + where: { + serviceVisibility: 'PUBLIC', + }, + }, }, }, }, @@ -530,6 +534,7 @@ const showFiltersId = 'show-filters' hx-trigger="input delay:500ms, keyup[key=='Enter']" hx-target={`#${searchResultsId}`} hx-select={`#${searchResultsId}`} + hx-swap="outerHTML" hx-push-url="true" hx-indicator="#search-indicator" class="contents"