diff --git a/apps/api/src/locales/@vitnode/core/en.json b/apps/api/src/locales/@vitnode/core/en.json index ebfd410cf..8a78cce11 100644 --- a/apps/api/src/locales/@vitnode/core/en.json +++ b/apps/api/src/locales/@vitnode/core/en.json @@ -103,6 +103,7 @@ "select_option": "Select an option", "select_options": "Select options", "select_language": "Select language", + "pick_color": "Pick a color", "go_to_prev_page": "Go to previous page", "go_to_next_page": "Go to next page", "errors": { diff --git a/apps/docs/content/docs/ui/color.mdx b/apps/docs/content/docs/ui/color.mdx new file mode 100644 index 000000000..74771ac13 --- /dev/null +++ b/apps/docs/content/docs/ui/color.mdx @@ -0,0 +1,97 @@ +--- +title: Color +description: Color picker field built on react-colorful that outputs an HSL string. +--- + +## Preview + + + +## Usage + +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; + + + +```ts +import { z } from "zod"; +import { AutoForm } from "@vitnode/core/components/form/auto-form"; +import { AutoFormColor } from "@vitnode/core/components/form/fields/color"; +``` + +```ts +const formSchema = z.object({ + color: z.string().default("hsl(240, 80%, 60%)"), +}); +``` + +```tsx + , + }, + ]} +/> +``` + + + + +```ts +import { ColorPicker } from "@vitnode/core/components/ui/color-picker"; +``` + +The `ColorPicker` is controlled - pass the current `value` (an HSL string) and +handle `onChange`, which receives the new HSL string: + +```tsx +const [color, setColor] = useState("hsl(240, 80%, 60%)"); + +; +``` + + + + + + Dragging the picker produces an HSL string such as `hsl(240, 80%, 60%)`. Below + the picker is a text field where you can paste or type a color in any CSS + format - hex (`#22c55e`), `rgb(...)`, `hsl(...)`, or a named color - so the + stored value is whatever you enter. The picker is powered by + [react-colorful](https://github.com/omgovich/react-colorful) and is + lazy-loaded, so its bundle only ships once the popover is opened. + + +## Props + +import { TypeTable } from "fumadocs-ui/components/type-table"; + + diff --git a/apps/docs/content/docs/ui/meta.json b/apps/docs/content/docs/ui/meta.json index 288445241..3918c4b3a 100644 --- a/apps/docs/content/docs/ui/meta.json +++ b/apps/docs/content/docs/ui/meta.json @@ -15,6 +15,7 @@ "---Forms---", "auto-form", "checkbox", + "color", "combobox", "editor", "input", diff --git a/apps/docs/migrations/0009_modern_eternals.sql b/apps/docs/migrations/0009_modern_eternals.sql new file mode 100644 index 000000000..c652e96dc --- /dev/null +++ b/apps/docs/migrations/0009_modern_eternals.sql @@ -0,0 +1 @@ +ALTER TABLE "core_roles" ALTER COLUMN "color" SET DATA TYPE varchar(50); \ No newline at end of file diff --git a/apps/docs/migrations/meta/0009_snapshot.json b/apps/docs/migrations/meta/0009_snapshot.json new file mode 100644 index 000000000..7f1768915 --- /dev/null +++ b/apps/docs/migrations/meta/0009_snapshot.json @@ -0,0 +1,1880 @@ +{ + "id": "7fd0cead-725d-42f5-bc00-7e1d8954ea75", + "prevId": "4829773c-6615-48c5-bebb-5ece6e9a38a5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.core_admin_permissions": { + "name": "core_admin_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_admin_permissions_role_id_idx": { + "name": "core_admin_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_permissions_user_id_idx": { + "name": "core_admin_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_permissions_roleId_core_roles_id_fk": { + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_permissions_userId_core_users_id_fk": { + "name": "core_admin_permissions_userId_core_users_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_admin_sessions": { + "name": "core_admin_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_admin_sessions_token_idx": { + "name": "core_admin_sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_sessions_user_id_idx": { + "name": "core_admin_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_sessions_userId_core_users_id_fk": { + "name": "core_admin_sessions_userId_core_users_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_admin_sessions_token_unique": { + "name": "core_admin_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_cron": { + "name": "core_cron", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "lastRun": { + "name": "lastRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "module": { + "name": "module", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "nextRun": { + "name": "nextRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_files": { + "name": "core_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_files_user_id_idx": { + "name": "core_files_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_files_userId_core_users_id_fk": { + "name": "core_files_userId_core_users_id_fk", + "tableFrom": "core_files", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_files_key_unique": { + "name": "core_files_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages": { + "name": "core_languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "time24": { + "name": "time24", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_languages_code_idx": { + "name": "core_languages_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_languages_name_idx": { + "name": "core_languages_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_languages_code_unique": { + "name": "core_languages_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages_words": { + "name": "core_languages_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "languageCode": { + "name": "languageCode", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pluginCode": { + "name": "pluginCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "itemId": { + "name": "itemId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tableName": { + "name": "tableName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "variable": { + "name": "variable", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_languages_words_lang_code_idx": { + "name": "core_languages_words_lang_code_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_languages_words_languageCode_core_languages_code_fk": { + "name": "core_languages_words_languageCode_core_languages_code_fk", + "tableFrom": "core_languages_words", + "tableTo": "core_languages", + "columnsFrom": [ + "languageCode" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_logs": { + "name": "core_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'GET'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'localhost'" + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "statusCode": { + "name": "statusCode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "test123": { + "name": "test123", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "core_logs_userId_core_users_id_fk": { + "name": "core_logs_userId_core_users_id_fk", + "tableFrom": "core_logs", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_moderators_permissions": { + "name": "core_moderators_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_moderators_permissions_role_id_idx": { + "name": "core_moderators_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_moderators_permissions_user_id_idx": { + "name": "core_moderators_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_moderators_permissions_roleId_core_roles_id_fk": { + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_moderators_permissions_userId_core_users_id_fk": { + "name": "core_moderators_permissions_userId_core_users_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_queue": { + "name": "core_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "queue": { + "name": "queue", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxAttempts": { + "name": "maxAttempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "availableAt": { + "name": "availableAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reservedAt": { + "name": "reservedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastError": { + "name": "lastError", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "core_queue_status_available_at_idx": { + "name": "core_queue_status_available_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "availableAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_roles": { + "name": "core_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "root": { + "name": "root", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "guest": { + "name": "guest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions": { + "name": "core_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_sessions_user_id_idx": { + "name": "core_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_sessions_userId_core_users_id_fk": { + "name": "core_sessions_userId_core_users_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_token_unique": { + "name": "core_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions_known_devices": { + "name": "core_sessions_known_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_sessions_known_devices_ip_address_idx": { + "name": "core_sessions_known_devices_ip_address_idx", + "columns": [ + { + "expression": "ipAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_known_devices_publicId_unique": { + "name": "core_sessions_known_devices_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users": { + "name": "core_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "nameCode": { + "name": "nameCode", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "newsletter": { + "name": "newsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatarColor": { + "name": "avatarColor", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "birthday": { + "name": "birthday", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": { + "core_users_name_code_idx": { + "name": "core_users_name_code_idx", + "columns": [ + { + "expression": "nameCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_name_idx": { + "name": "core_users_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_email_idx": { + "name": "core_users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_roleId_core_roles_id_fk": { + "name": "core_users_roleId_core_roles_id_fk", + "tableFrom": "core_users", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "core_users_language_core_languages_code_fk": { + "name": "core_users_language_core_languages_code_fk", + "tableFrom": "core_users", + "tableTo": "core_languages", + "columnsFrom": [ + "language" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set default", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_nameCode_unique": { + "name": "core_users_nameCode_unique", + "nullsNotDistinct": false, + "columns": [ + "nameCode" + ] + }, + "core_users_name_unique": { + "name": "core_users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "core_users_email_unique": { + "name": "core_users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_confirm_emails": { + "name": "core_users_confirm_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_confirm_emails_userId_core_users_id_fk": { + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "tableFrom": "core_users_confirm_emails", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_confirm_emails_token_unique": { + "name": "core_users_confirm_emails_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_forgot_password": { + "name": "core_users_forgot_password", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_forgot_password_userId_core_users_id_fk": { + "name": "core_users_forgot_password_userId_core_users_id_fk", + "tableFrom": "core_users_forgot_password", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_forgot_password_userId_unique": { + "name": "core_users_forgot_password_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + }, + "core_users_forgot_password_token_unique": { + "name": "core_users_forgot_password_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_secondary_roles": { + "name": "core_users_secondary_roles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_users_secondary_roles_user_id_idx": { + "name": "core_users_secondary_roles_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_secondary_roles_role_id_idx": { + "name": "core_users_secondary_roles_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_secondary_roles_userId_core_users_id_fk": { + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_users_secondary_roles_roleId_core_roles_id_fk": { + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "core_users_secondary_roles_userId_roleId_pk": { + "name": "core_users_secondary_roles_userId_roleId_pk", + "columns": [ + "userId", + "roleId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_sso": { + "name": "core_users_sso", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_users_sso_user_id_idx": { + "name": "core_users_sso_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_sso_userId_core_users_id_fk": { + "name": "core_users_sso_userId_core_users_id_fk", + "tableFrom": "core_users_sso", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_categories": { + "name": "blog_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_categories_titleSeo_unique": { + "name": "blog_categories_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_posts": { + "name": "blog_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "categoryId": { + "name": "categoryId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "blog_posts_categoryId_blog_categories_id_fk": { + "name": "blog_posts_categoryId_blog_categories_id_fk", + "tableFrom": "blog_posts", + "tableTo": "blog_categories", + "columnsFrom": [ + "categoryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_posts_titleSeo_unique": { + "name": "blog_posts_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/docs/migrations/meta/_journal.json b/apps/docs/migrations/meta/_journal.json index affe4d6d8..afc6ccdd9 100644 --- a/apps/docs/migrations/meta/_journal.json +++ b/apps/docs/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1783283845218, "tag": "0008_silky_forgotten_one", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1783880180694, + "tag": "0009_modern_eternals", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/docs/src/examples/color.tsx b/apps/docs/src/examples/color.tsx new file mode 100644 index 000000000..95781f7cf --- /dev/null +++ b/apps/docs/src/examples/color.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { AutoForm } from "@vitnode/core/components/form/auto-form"; +import { AutoFormColor } from "@vitnode/core/components/form/fields/color"; +import { z } from "zod"; + +export default function ColorExample() { + const formSchema = z.object({ + color: z.string().default("hsl(240, 80%, 60%)"), + }); + + return ( + ( + + ), + }, + ]} + formSchema={formSchema} + onSubmit={values => { + // eslint-disable-next-line no-console + console.log(values.color); + }} + /> + ); +} diff --git a/apps/docs/src/locales/@vitnode/core/en.json b/apps/docs/src/locales/@vitnode/core/en.json index ebfd410cf..8a78cce11 100644 --- a/apps/docs/src/locales/@vitnode/core/en.json +++ b/apps/docs/src/locales/@vitnode/core/en.json @@ -103,6 +103,7 @@ "select_option": "Select an option", "select_options": "Select options", "select_language": "Select language", + "pick_color": "Pick a color", "go_to_prev_page": "Go to previous page", "go_to_next_page": "Go to next page", "errors": { diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index a88a7f1f1..548617acf 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -128,6 +128,7 @@ "next-themes": "^0.4.6", "postgres": "^3.4.9", "rate-limiter-flexible": "^11.2.0", + "react-colorful": "^5.6.1", "react-day-picker": "^10.0.1", "react-resizable-panels": "^4.11.2", "react-scan": "^0.5.7", diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts index e6769ff3f..ca7b4a77c 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts @@ -18,7 +18,7 @@ export const zodRoleNameSchema = z export const zodCreateRoleAdminSchema = z.object({ name: zodRoleNameSchema, - color: z.string().max(19).optional(), + color: z.string().max(50).optional(), }); export const createRoleAdminRoute = buildRoute({ diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts index 4afab926d..f7293838f 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts @@ -11,7 +11,7 @@ import { zodRoleNameSchema } from "./create.route"; export const zodUpdateRoleAdminSchema = z .object({ name: zodRoleNameSchema, - color: z.string().max(19), + color: z.string().max(50), }) .partial() .refine(body => Object.values(body).some(value => value !== undefined), { diff --git a/packages/vitnode/src/components/form/fields/color.test.tsx b/packages/vitnode/src/components/form/fields/color.test.tsx new file mode 100644 index 000000000..4dbb9979b --- /dev/null +++ b/packages/vitnode/src/components/form/fields/color.test.tsx @@ -0,0 +1,135 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { type FieldValues, useForm } from "react-hook-form"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { Form, FormField } from "@/components/ui/form"; + +import { AutoFormColor } from "./color"; + +vi.mock("next-intl", () => ({ + useLocale: () => "en", + useTranslations: () => (key: string) => key, +})); + +vi.mock("react-colorful", () => ({ + HslStringColorPicker: ({ + color, + onChange, + }: { + color?: string; + onChange?: (value: string) => void; + }) => ( + + ), +})); + +const Harness = ({ + onSubmit = vi.fn(), + defaultValue = "hsl(240, 80%, 60%)", +}: { + defaultValue?: string; + onSubmit?: (values: FieldValues) => void; +}) => { + const form = useForm({ + defaultValues: { color: defaultValue } as FieldValues, + }); + + return ( +
+ ( + + )} + /> + + + ); +}; + +describe("AutoFormColor", () => { + beforeEach(() => { + Element.prototype.scrollIntoView = vi.fn(); + Element.prototype.hasPointerCapture = vi.fn(() => false); + Element.prototype.setPointerCapture = vi.fn(); + Element.prototype.releasePointerCapture = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("renders the label and the current color on the trigger", () => { + render(); + + expect(screen.getByText("Color")).toBeDefined(); + expect(screen.getByText("hsl(240, 80%, 60%)")).toBeDefined(); + }); + + it("shows the pick-color placeholder when no color is set", () => { + render(); + + expect(screen.getByText("pick_color")).toBeDefined(); + }); + + it("writes the picked HSL color into the form and submits it", async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.click(screen.getByText("hsl(240, 80%, 60%)")); + fireEvent.click(await screen.findByTestId("picker")); + + fireEvent.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + { color: "hsl(200, 50%, 50%)" }, + expect.anything(), + ); + }); + }); + + it("lets the user type a color in any format via the text input", async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.click(screen.getByText("hsl(240, 80%, 60%)")); + const input = await screen.findByRole("textbox"); + fireEvent.change(input, { target: { value: "#00ff00" } }); + + fireEvent.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + { color: "#00ff00" }, + expect.anything(), + ); + }); + }); + + it("clears the value via the remove button when allowRemoveColor is set", async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.click(screen.getByText("hsl(240, 80%, 60%)")); + fireEvent.click(await screen.findByRole("button", { name: "remove" })); + + fireEvent.click(screen.getByRole("button", { name: "submit" })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith({ color: "" }, expect.anything()); + }); + }); +}); diff --git a/packages/vitnode/src/components/form/fields/color.tsx b/packages/vitnode/src/components/form/fields/color.tsx new file mode 100644 index 000000000..10f5b5ffa --- /dev/null +++ b/packages/vitnode/src/components/form/fields/color.tsx @@ -0,0 +1,43 @@ +import type React from "react"; + +import { ColorPicker } from "@/components/ui/color-picker"; +import { FormControl, FormMessage } from "@/components/ui/form"; + +import type { ItemAutoFormComponentProps } from "../auto-form"; + +import { AutoFormDesc } from "../common/desc"; +import { AutoFormLabel } from "../common/label"; + +export const AutoFormColor = ({ + label, + labelRight, + description, + otherProps: { isOptional }, + field, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + itemParams, + ...props +}: ItemAutoFormComponentProps & + Omit, "onChange" | "value">) => { + return ( + <> + {label && ( + + {label} + + )} + + + + + + {description && {description}} + + + ); +}; diff --git a/packages/vitnode/src/components/ui/color-picker.tsx b/packages/vitnode/src/components/ui/color-picker.tsx new file mode 100644 index 000000000..b1869ee28 --- /dev/null +++ b/packages/vitnode/src/components/ui/color-picker.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { XIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import React from "react"; + +import { cn } from "@/lib/utils"; + +import { Button } from "./button"; +import { Input } from "./input"; +import { Loader } from "./loader"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; + +// react-colorful only ships in the bundle once the picker is actually opened. +const HslStringColorPicker = React.lazy(async () => ({ + default: (await import("react-colorful")).HslStringColorPicker, +})); + +export const ColorPicker = ({ + value = "", + onChange, + placeholder, + allowRemoveColor, + className, + ...props +}: Omit, "children" | "onChange" | "value"> & { + allowRemoveColor?: boolean; + onChange?: (value: string) => void; + placeholder?: string; + value?: string; +}) => { + const t = useTranslations("core.global"); + + return ( + + + } + > + + + {value || (placeholder ?? t("pick_color"))} + + + + + + + + } + > + + + + onChange?.(event.target.value)} + placeholder={placeholder ?? "hsl(240, 80%, 60%)"} + value={value} + /> + + {allowRemoveColor && value && ( + + )} + + + ); +}; diff --git a/packages/vitnode/src/database/roles.ts b/packages/vitnode/src/database/roles.ts index 7b5a3601d..7c8dcb859 100644 --- a/packages/vitnode/src/database/roles.ts +++ b/packages/vitnode/src/database/roles.ts @@ -11,5 +11,5 @@ export const core_roles = pgTable("core_roles", t => ({ default: t.boolean().notNull().default(false), root: t.boolean().notNull().default(false), guest: t.boolean().notNull().default(false), - color: t.varchar({ length: 19 }), + color: t.varchar({ length: 50 }), })).enableRLS(); diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index ebfd410cf..8a78cce11 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -103,6 +103,7 @@ "select_option": "Select an option", "select_options": "Select options", "select_language": "Select language", + "pick_color": "Pick a color", "go_to_prev_page": "Go to previous page", "go_to_next_page": "Go to next page", "errors": { diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx index 82cf5792e..11e523043 100644 --- a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.test.tsx @@ -89,10 +89,10 @@ describe("CreateEditRoleAdmin", () => { ], }); - const [nameInput, colorInput] = - screen.getAllByRole("textbox"); + // The name is a text input; the color renders on the color-picker trigger. + const [nameInput] = screen.getAllByRole("textbox"); expect(nameInput.value).toBe("Admin"); - expect(colorInput.value).toBe("#ef4444"); + expect(screen.getByText("#ef4444")).toBeDefined(); submitForm(nameInput); diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx index 229a2b532..0be05d0a4 100644 --- a/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/roles/actions/create-edit/create-edit.tsx @@ -3,6 +3,7 @@ import { toast } from "sonner"; import { z } from "zod"; import { AutoForm, type AutoFormOnSubmit } from "@/components/form/auto-form"; +import { AutoFormColor } from "@/components/form/fields/color"; import { AutoFormInput } from "@/components/form/fields/input"; import { useDialog } from "@/components/ui/dialog"; import { multiLangValueSchema } from "@/lib/helpers/multi-lang"; @@ -34,7 +35,7 @@ export const CreateEditRoleAdmin = ({ data }: { data?: RoleData }) => { ), color: z .string() - .max(19) + .max(50) .default(data?.color ?? ""), }); @@ -68,9 +69,9 @@ export const CreateEditRoleAdmin = ({ data }: { data?: RoleData }) => { { id: "color", component: props => ( - ), diff --git a/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx b/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx index d29fe0ff9..2d7250390 100644 --- a/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/roles/roles-admin-view.tsx @@ -66,7 +66,7 @@ export const RolesAdminView = async ({ { accessorKey: "updatedAt", header: t("updatedAt"), - cell: ({ row }) => , + cell: ({ row }) => , }, { id: "actions", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 028a2026b..4af932786 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,6 +530,9 @@ importers: rate-limiter-flexible: specifier: ^11.2.0 version: 11.2.0 + react-colorful: + specifier: ^5.6.1 + version: 5.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-day-picker: specifier: ^10.0.1 version: 10.0.1(@types/react@19.2.17)(react@19.2.7) @@ -7875,6 +7878,12 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + react-colorful@5.7.0: + resolution: {integrity: sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-day-picker@10.0.1: resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==} engines: {node: '>=18'} @@ -16088,6 +16097,11 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + react-colorful@5.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-day-picker@10.0.1(@types/react@19.2.17)(react@19.2.7): dependencies: '@date-fns/tz': 1.5.0