From a5d5ead38a8a6e1375a1830a46ac330227999a1e Mon Sep 17 00:00:00 2001 From: Aran Roig Date: Sun, 20 Sep 2026 12:50:40 +0200 Subject: [PATCH] More colors --- backend/src/routes/hibolia.js | 8 +- backend/src/schemas/HiboliaOrder.js | 22 +++++ frontend/app/pages/admin/index.vue | 19 +++- frontend/app/pages/projects/hibolia/order.vue | 95 +++++++++++++++++-- frontend/i18n/locales/ca.json | 11 +++ frontend/i18n/locales/en.json | 11 +++ frontend/i18n/locales/es.json | 11 +++ frontend/i18n/locales/zh.json | 11 +++ 8 files changed, 178 insertions(+), 10 deletions(-) diff --git a/backend/src/routes/hibolia.js b/backend/src/routes/hibolia.js index fbd5ab0..eba7424 100644 --- a/backend/src/routes/hibolia.js +++ b/backend/src/routes/hibolia.js @@ -5,11 +5,13 @@ const { requireAuth } = require("../middleware/auth"); const router = express.Router(); const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/; // POST /api/hibolia/orders — public order submission router.post("/orders", async (req, res) => { try { - const { name, surname, email, message, color, shipping } = req.body || {}; + const { name, surname, email, message, color, customColor, shipping } = + req.body || {}; if (!name || !surname || !email || !message || !color || !shipping) { return res .status(400) @@ -18,6 +20,9 @@ router.post("/orders", async (req, res) => { if (!HiboliaOrder.COLORS.includes(color)) { return res.status(400).json({ error: "invalid color" }); } + if (color === "custom" && !HEX_COLOR_RE.test(String(customColor || ""))) { + return res.status(400).json({ error: "invalid custom color" }); + } if (!EMAIL_RE.test(String(email))) { return res.status(400).json({ error: "invalid email" }); } @@ -27,6 +32,7 @@ router.post("/orders", async (req, res) => { email, message, color, + customColor: color === "custom" ? String(customColor).toLowerCase() : undefined, shipping, }); res.status(201).json({ id: order._id }); diff --git a/backend/src/schemas/HiboliaOrder.js b/backend/src/schemas/HiboliaOrder.js index 9eef8bb..eee4e3c 100644 --- a/backend/src/schemas/HiboliaOrder.js +++ b/backend/src/schemas/HiboliaOrder.js @@ -12,9 +12,19 @@ const COLORS = [ "black", "white", "pink", + "turquoise", + "teal", + "navy", + "lavender", + "magenta", + "lime", + "coral", + "custom", "none", ]; +const HEX_COLOR_RE = /^#[0-9a-f]{6}$/; + const hiboliaOrderSchema = new mongoose.Schema( { name: { type: String, required: true, trim: true, maxlength: 100 }, @@ -22,6 +32,18 @@ const hiboliaOrderSchema = new mongoose.Schema( email: { type: String, required: true, trim: true, lowercase: true }, message: { type: String, required: true, trim: true, maxlength: 2000 }, color: { type: String, required: true, enum: COLORS }, + customColor: { + type: String, + trim: true, + lowercase: true, + required: [ + function () { + return this.color === "custom"; + }, + "customColor is required when color is custom", + ], + match: [HEX_COLOR_RE, "invalid custom color"], + }, shipping: { type: String, required: true, trim: true, maxlength: 2000 }, }, { timestamps: true } diff --git a/frontend/app/pages/admin/index.vue b/frontend/app/pages/admin/index.vue index 1fdd8fb..c37238c 100644 --- a/frontend/app/pages/admin/index.vue +++ b/frontend/app/pages/admin/index.vue @@ -22,9 +22,22 @@ const COLOR_HEX: Record = { yellow: '#e2cf3c', green: '#5fae4e', blue: '#4c8be0', - purple: '#9b59d0' + purple: '#9b59d0', + black: '#1a1a1a', + white: '#f2f2f2', + pink: '#e88fb3', + turquoise: '#40c4b8', + teal: '#17857c', + navy: '#34549c', + lavender: '#b98fe0', + magenta: '#cf479f', + lime: '#a8d14e', + coral: '#ef8368' }; +const orderSwatch = (order: any) => + order.color === 'custom' ? order.customColor : COLOR_HEX[order.color] || 'transparent'; + const formatUptime = (seconds: number) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); @@ -153,8 +166,8 @@ const onLogout = () => redirectToLogin(); {{ order.message }} - - {{ $t(`order.hibolia.colors.${order.color}`) }} + + {{ $t(`order.hibolia.colors.${order.color}`) }} {{ order.shipping }} diff --git a/frontend/app/pages/projects/hibolia/order.vue b/frontend/app/pages/projects/hibolia/order.vue index ee9e9a2..99f9f75 100644 --- a/frontend/app/pages/projects/hibolia/order.vue +++ b/frontend/app/pages/projects/hibolia/order.vue @@ -23,23 +23,37 @@ const COLOR_OPTIONS = [ { value: 'black', hex: '#1a1a1a' }, { value: 'white', hex: '#f2f2f2' }, { value: 'pink', hex: '#e88fb3' }, + { value: 'turquoise', hex: '#40c4b8' }, + { value: 'teal', hex: '#17857c' }, + { value: 'navy', hex: '#34549c' }, + { value: 'lavender', hex: '#b98fe0' }, + { value: 'magenta', hex: '#cf479f' }, + { value: 'lime', hex: '#a8d14e' }, + { value: 'coral', hex: '#ef8368' }, + { value: 'custom', hex: '#40c4b8' }, { value: 'none', hex: '#9e9e9e' } ]; +const DEFAULT_CUSTOM_COLOR = '#40c4b8'; +const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/; + const form = reactive({ name: '', surname: '', email: '', message: '', - color: '', + color: 'none', + customColor: DEFAULT_CUSTOM_COLOR, shipping: '' }); const DEFAULT_EDGE_COLOR = '#d8d2c6'; -const selectedColorHex = computed( - () => COLOR_OPTIONS.find((o) => o.value === form.color)?.hex ?? DEFAULT_EDGE_COLOR -); +const selectedColorHex = computed(() => { + if (form.color === 'custom') return form.customColor; + if (form.color === 'none') return DEFAULT_EDGE_COLOR; + return COLOR_OPTIONS.find((o) => o.value === form.color)?.hex ?? DEFAULT_EDGE_COLOR; +}); const submitting = ref(false); const errorMsg = ref(''); @@ -62,6 +76,11 @@ const onSubmit = async () => { return; } + if (form.color === 'custom' && !HEX_COLOR_RE.test(form.customColor)) { + errorMsg.value = $t('order.hibolia.error_invalid_color'); + return; + } + submitting.value = true; try { await post('/hibolia/orders', { @@ -70,13 +89,16 @@ const onSubmit = async () => { email: form.email.trim(), message: form.message.trim(), color: form.color, + customColor: + form.color === 'custom' ? form.customColor.toLowerCase() : undefined, shipping: form.shipping.trim() }); form.name = ''; form.surname = ''; form.email = ''; form.message = ''; - form.color = ''; + form.color = 'none'; + form.customColor = DEFAULT_CUSTOM_COLOR; form.shipping = ''; await navigateTo(localePath('/projects/hibolia/thanks')); } catch (e: any) { @@ -185,10 +207,28 @@ const onSubmit = async () => { :disabled="submitting" :id="`order-color-${option.value}`" /> - + {{ $t(`order.hibolia.colors.${option.value}`) }} +
+ + + {{ form.customColor }} +
+

+ {{ $t('order.hibolia.custom_color_note') }} +

{{ $t('order.hibolia.preview_note') }}

@@ -383,6 +423,49 @@ const onSubmit = async () => { gap: 8px; } +.custom-color-row { + display: flex; + align-items: center; + gap: 10px; + margin-top: 10px; + + label { + font-family: 'Hurmit', monospace; + font-size: 0.9rem; + color: var(--color-link); + min-width: 0; + text-align: left; + } + + input[type='color'] { + width: 48px; + height: 30px; + padding: 2px; + background: var(--color-background); + border: 1px solid var(--color-border-color); + cursor: pointer; + + &:focus-visible { + outline: 1px dashed var(--color-link); + outline-offset: 2px; + } + } + + .custom-color-hex { + font-family: 'Hurmit', monospace; + font-size: 0.8rem; + color: var(--color-text); + } +} + +.custom-color-note { + font-family: 'Hurmit', monospace; + font-size: 0.8rem; + color: var(--color-text); + opacity: 0.75; + margin: 8px 0 0; +} + .deck-preview { height: 300px; margin: 0 0 14px; diff --git a/frontend/i18n/locales/ca.json b/frontend/i18n/locales/ca.json index f8abe53..72d2def 100644 --- a/frontend/i18n/locales/ca.json +++ b/frontend/i18n/locales/ca.json @@ -69,6 +69,8 @@ "email_label": "correu", "message_label": "missatge", "color_label": "color", + "custom_color_label": "color personalitzat", + "custom_color_note": "El pintaré amb el color que més s'assembli al que has seleccionat", "shipping_label": "enviament", "colors": { "red": "Vermell", @@ -82,6 +84,14 @@ "black": "Negre", "white": "Blanc", "pink": "Rosa", + "turquoise": "Turquesa", + "teal": "Verd blavós", + "navy": "Blau marí", + "lavender": "Llavanda", + "magenta": "Magenta", + "lime": "Verd llima", + "coral": "Coral", + "custom": "Personalitzat", "none": "Cap" }, "submit": "enviar comanda", @@ -89,6 +99,7 @@ "success": "Comanda enviada correctament.", "error_required": "Tots els camps són obligatoris.", "error_invalid": "Algunes dades no són vàlides. Revisa el formulari i torna-ho a provar.", + "error_invalid_color": "El color personalitzat no és vàlid.", "error_generic": "No s'ha pogut enviar la comanda. Torna-ho a provar més tard.", "form_title": "Formulari de comanda de baralla de cartes", "intro": "Aquest formulari servirà per registrar oficialment la teva comanda de baralla de cartes.", diff --git a/frontend/i18n/locales/en.json b/frontend/i18n/locales/en.json index 81f0824..db87ac1 100644 --- a/frontend/i18n/locales/en.json +++ b/frontend/i18n/locales/en.json @@ -69,6 +69,8 @@ "email_label": "email", "message_label": "message", "color_label": "color", + "custom_color_label": "custom color", + "custom_color_note": "I will paint it with the color that most closely resembles the one you selected", "shipping_label": "shipping", "colors": { "red": "Red", @@ -82,6 +84,14 @@ "black": "Black", "white": "White", "pink": "Pink", + "turquoise": "Turquoise", + "teal": "Teal", + "navy": "Navy", + "lavender": "Lavender", + "magenta": "Magenta", + "lime": "Lime", + "coral": "Coral", + "custom": "Custom", "none": "None" }, "submit": "send order", @@ -89,6 +99,7 @@ "success": "Order sent successfully.", "error_required": "All fields are required.", "error_invalid": "Some data is invalid. Check the form and try again.", + "error_invalid_color": "The custom color is not valid.", "error_generic": "Could not send the order. Try again later.", "form_title": "Playing card deck order form", "intro": "This form will be used to officially register your playing card deck order.", diff --git a/frontend/i18n/locales/es.json b/frontend/i18n/locales/es.json index d232bed..baea27a 100644 --- a/frontend/i18n/locales/es.json +++ b/frontend/i18n/locales/es.json @@ -69,6 +69,8 @@ "email_label": "email", "message_label": "mensaje", "color_label": "color", + "custom_color_label": "color personalizado", + "custom_color_note": "Lo pintaré con el color que más se parezca al que has seleccionado", "shipping_label": "envío", "colors": { "red": "Rojo", @@ -82,6 +84,14 @@ "black": "Negro", "white": "Blanco", "pink": "Rosa", + "turquoise": "Turquesa", + "teal": "Verde azulado", + "navy": "Azul marino", + "lavender": "Lavanda", + "magenta": "Magenta", + "lime": "Verde lima", + "coral": "Coral", + "custom": "Personalizado", "none": "Ninguno" }, "submit": "enviar pedido", @@ -89,6 +99,7 @@ "success": "Pedido enviado correctamente.", "error_required": "Todos los campos son obligatorios.", "error_invalid": "Algunos datos no son válidos. Revisa el formulario e inténtalo de nuevo.", + "error_invalid_color": "El color personalizado no es válido.", "error_generic": "No se pudo enviar el pedido. Inténtalo más tarde.", "form_title": "Formulario de orden de baraja de cartas", "intro": "Este formulario será para registrar oficialmente tu pedido de baraja de cartas.", diff --git a/frontend/i18n/locales/zh.json b/frontend/i18n/locales/zh.json index 5f4e510..0486756 100644 --- a/frontend/i18n/locales/zh.json +++ b/frontend/i18n/locales/zh.json @@ -69,6 +69,8 @@ "email_label": "邮箱", "message_label": "留言", "color_label": "颜色", + "custom_color_label": "自定义颜色", + "custom_color_note": "我会用与您所选颜色最接近的颜色来绘制", "shipping_label": "配送", "colors": { "red": "红色", @@ -82,6 +84,14 @@ "black": "黑色", "white": "白色", "pink": "粉色", + "turquoise": "青绿色", + "teal": "蓝绿色", + "navy": "藏青色", + "lavender": "淡紫色", + "magenta": "洋红色", + "lime": "黄绿色", + "coral": "珊瑚色", + "custom": "自定义", "none": "无" }, "submit": "提交订单", @@ -89,6 +99,7 @@ "success": "订单提交成功。", "error_required": "所有字段均为必填。", "error_invalid": "部分数据无效,请检查表单后重试。", + "error_invalid_color": "自定义颜色无效。", "error_generic": "无法提交订单,请稍后再试。", "form_title": "纸牌订购表单", "intro": "此表单用于正式登记您的纸牌订单。",