More colors
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 50s

This commit is contained in:
2026-09-20 12:50:40 +02:00
parent f19f482433
commit a5d5ead38a
8 changed files with 178 additions and 10 deletions

View File

@@ -5,11 +5,13 @@ const { requireAuth } = require("../middleware/auth");
const router = express.Router(); const router = express.Router();
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
// POST /api/hibolia/orders — public order submission // POST /api/hibolia/orders — public order submission
router.post("/orders", async (req, res) => { router.post("/orders", async (req, res) => {
try { 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) { if (!name || !surname || !email || !message || !color || !shipping) {
return res return res
.status(400) .status(400)
@@ -18,6 +20,9 @@ router.post("/orders", async (req, res) => {
if (!HiboliaOrder.COLORS.includes(color)) { if (!HiboliaOrder.COLORS.includes(color)) {
return res.status(400).json({ error: "invalid 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))) { if (!EMAIL_RE.test(String(email))) {
return res.status(400).json({ error: "invalid email" }); return res.status(400).json({ error: "invalid email" });
} }
@@ -27,6 +32,7 @@ router.post("/orders", async (req, res) => {
email, email,
message, message,
color, color,
customColor: color === "custom" ? String(customColor).toLowerCase() : undefined,
shipping, shipping,
}); });
res.status(201).json({ id: order._id }); res.status(201).json({ id: order._id });

View File

@@ -12,9 +12,19 @@ const COLORS = [
"black", "black",
"white", "white",
"pink", "pink",
"turquoise",
"teal",
"navy",
"lavender",
"magenta",
"lime",
"coral",
"custom",
"none", "none",
]; ];
const HEX_COLOR_RE = /^#[0-9a-f]{6}$/;
const hiboliaOrderSchema = new mongoose.Schema( const hiboliaOrderSchema = new mongoose.Schema(
{ {
name: { type: String, required: true, trim: true, maxlength: 100 }, 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 }, email: { type: String, required: true, trim: true, lowercase: true },
message: { type: String, required: true, trim: true, maxlength: 2000 }, message: { type: String, required: true, trim: true, maxlength: 2000 },
color: { type: String, required: true, enum: COLORS }, 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 }, shipping: { type: String, required: true, trim: true, maxlength: 2000 },
}, },
{ timestamps: true } { timestamps: true }

View File

@@ -22,9 +22,22 @@ const COLOR_HEX: Record<string, string> = {
yellow: '#e2cf3c', yellow: '#e2cf3c',
green: '#5fae4e', green: '#5fae4e',
blue: '#4c8be0', 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 formatUptime = (seconds: number) => {
const h = Math.floor(seconds / 3600); const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60); const m = Math.floor((seconds % 3600) / 60);
@@ -153,8 +166,8 @@ const onLogout = () => redirectToLogin();
<td class="msg-cell">{{ order.message }}</td> <td class="msg-cell">{{ order.message }}</td>
<td> <td>
<span class="color-chip"> <span class="color-chip">
<span class="swatch" :style="{ background: COLOR_HEX[order.color] || 'transparent' }" aria-hidden="true"></span> <span class="swatch" :style="{ background: orderSwatch(order) }" aria-hidden="true"></span>
{{ $t(`order.hibolia.colors.${order.color}`) }} {{ $t(`order.hibolia.colors.${order.color}`) }}<template v-if="order.color === 'custom'"> {{ order.customColor }}</template>
</span> </span>
</td> </td>
<td class="msg-cell">{{ order.shipping }}</td> <td class="msg-cell">{{ order.shipping }}</td>

View File

@@ -23,23 +23,37 @@ const COLOR_OPTIONS = [
{ value: 'black', hex: '#1a1a1a' }, { value: 'black', hex: '#1a1a1a' },
{ value: 'white', hex: '#f2f2f2' }, { value: 'white', hex: '#f2f2f2' },
{ value: 'pink', hex: '#e88fb3' }, { 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' } { value: 'none', hex: '#9e9e9e' }
]; ];
const DEFAULT_CUSTOM_COLOR = '#40c4b8';
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const form = reactive({ const form = reactive({
name: '', name: '',
surname: '', surname: '',
email: '', email: '',
message: '', message: '',
color: '', color: 'none',
customColor: DEFAULT_CUSTOM_COLOR,
shipping: '' shipping: ''
}); });
const DEFAULT_EDGE_COLOR = '#d8d2c6'; const DEFAULT_EDGE_COLOR = '#d8d2c6';
const selectedColorHex = computed( const selectedColorHex = computed(() => {
() => COLOR_OPTIONS.find((o) => o.value === form.color)?.hex ?? DEFAULT_EDGE_COLOR 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 submitting = ref(false);
const errorMsg = ref(''); const errorMsg = ref('');
@@ -62,6 +76,11 @@ const onSubmit = async () => {
return; return;
} }
if (form.color === 'custom' && !HEX_COLOR_RE.test(form.customColor)) {
errorMsg.value = $t('order.hibolia.error_invalid_color');
return;
}
submitting.value = true; submitting.value = true;
try { try {
await post('/hibolia/orders', { await post('/hibolia/orders', {
@@ -70,13 +89,16 @@ const onSubmit = async () => {
email: form.email.trim(), email: form.email.trim(),
message: form.message.trim(), message: form.message.trim(),
color: form.color, color: form.color,
customColor:
form.color === 'custom' ? form.customColor.toLowerCase() : undefined,
shipping: form.shipping.trim() shipping: form.shipping.trim()
}); });
form.name = ''; form.name = '';
form.surname = ''; form.surname = '';
form.email = ''; form.email = '';
form.message = ''; form.message = '';
form.color = ''; form.color = 'none';
form.customColor = DEFAULT_CUSTOM_COLOR;
form.shipping = ''; form.shipping = '';
await navigateTo(localePath('/projects/hibolia/thanks')); await navigateTo(localePath('/projects/hibolia/thanks'));
} catch (e: any) { } catch (e: any) {
@@ -185,10 +207,28 @@ const onSubmit = async () => {
:disabled="submitting" :disabled="submitting"
:id="`order-color-${option.value}`" :id="`order-color-${option.value}`"
/> />
<span class="swatch" :style="{ background: option.hex }" aria-hidden="true"></span> <span
class="swatch"
:style="{ background: option.value === 'custom' ? form.customColor : option.hex }"
aria-hidden="true"
></span>
<span class="color-name">{{ $t(`order.hibolia.colors.${option.value}`) }}</span> <span class="color-name">{{ $t(`order.hibolia.colors.${option.value}`) }}</span>
</label> </label>
</div> </div>
<div v-if="form.color === 'custom'" class="custom-color-row">
<label for="order-custom-color">{{ $t('order.hibolia.custom_color_label') }}:</label>
<input
id="order-custom-color"
v-model="form.customColor"
type="color"
name="customColor"
:disabled="submitting"
/>
<span class="custom-color-hex">{{ form.customColor }}</span>
</div>
<p v-if="form.color === 'custom'" class="custom-color-note">
{{ $t('order.hibolia.custom_color_note') }}
</p>
</fieldset> </fieldset>
<p>{{ $t('order.hibolia.preview_note') }}</p> <p>{{ $t('order.hibolia.preview_note') }}</p>
@@ -383,6 +423,49 @@ const onSubmit = async () => {
gap: 8px; 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 { .deck-preview {
height: 300px; height: 300px;
margin: 0 0 14px; margin: 0 0 14px;

View File

@@ -69,6 +69,8 @@
"email_label": "correu", "email_label": "correu",
"message_label": "missatge", "message_label": "missatge",
"color_label": "color", "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", "shipping_label": "enviament",
"colors": { "colors": {
"red": "Vermell", "red": "Vermell",
@@ -82,6 +84,14 @@
"black": "Negre", "black": "Negre",
"white": "Blanc", "white": "Blanc",
"pink": "Rosa", "pink": "Rosa",
"turquoise": "Turquesa",
"teal": "Verd blavós",
"navy": "Blau marí",
"lavender": "Llavanda",
"magenta": "Magenta",
"lime": "Verd llima",
"coral": "Coral",
"custom": "Personalitzat",
"none": "Cap" "none": "Cap"
}, },
"submit": "enviar comanda", "submit": "enviar comanda",
@@ -89,6 +99,7 @@
"success": "Comanda enviada correctament.", "success": "Comanda enviada correctament.",
"error_required": "Tots els camps són obligatoris.", "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": "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.", "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", "form_title": "Formulari de comanda de baralla de cartes",
"intro": "Aquest formulari servirà per registrar oficialment la teva comanda de baralla de cartes.", "intro": "Aquest formulari servirà per registrar oficialment la teva comanda de baralla de cartes.",

View File

@@ -69,6 +69,8 @@
"email_label": "email", "email_label": "email",
"message_label": "message", "message_label": "message",
"color_label": "color", "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", "shipping_label": "shipping",
"colors": { "colors": {
"red": "Red", "red": "Red",
@@ -82,6 +84,14 @@
"black": "Black", "black": "Black",
"white": "White", "white": "White",
"pink": "Pink", "pink": "Pink",
"turquoise": "Turquoise",
"teal": "Teal",
"navy": "Navy",
"lavender": "Lavender",
"magenta": "Magenta",
"lime": "Lime",
"coral": "Coral",
"custom": "Custom",
"none": "None" "none": "None"
}, },
"submit": "send order", "submit": "send order",
@@ -89,6 +99,7 @@
"success": "Order sent successfully.", "success": "Order sent successfully.",
"error_required": "All fields are required.", "error_required": "All fields are required.",
"error_invalid": "Some data is invalid. Check the form and try again.", "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.", "error_generic": "Could not send the order. Try again later.",
"form_title": "Playing card deck order form", "form_title": "Playing card deck order form",
"intro": "This form will be used to officially register your playing card deck order.", "intro": "This form will be used to officially register your playing card deck order.",

View File

@@ -69,6 +69,8 @@
"email_label": "email", "email_label": "email",
"message_label": "mensaje", "message_label": "mensaje",
"color_label": "color", "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", "shipping_label": "envío",
"colors": { "colors": {
"red": "Rojo", "red": "Rojo",
@@ -82,6 +84,14 @@
"black": "Negro", "black": "Negro",
"white": "Blanco", "white": "Blanco",
"pink": "Rosa", "pink": "Rosa",
"turquoise": "Turquesa",
"teal": "Verde azulado",
"navy": "Azul marino",
"lavender": "Lavanda",
"magenta": "Magenta",
"lime": "Verde lima",
"coral": "Coral",
"custom": "Personalizado",
"none": "Ninguno" "none": "Ninguno"
}, },
"submit": "enviar pedido", "submit": "enviar pedido",
@@ -89,6 +99,7 @@
"success": "Pedido enviado correctamente.", "success": "Pedido enviado correctamente.",
"error_required": "Todos los campos son obligatorios.", "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": "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.", "error_generic": "No se pudo enviar el pedido. Inténtalo más tarde.",
"form_title": "Formulario de orden de baraja de cartas", "form_title": "Formulario de orden de baraja de cartas",
"intro": "Este formulario será para registrar oficialmente tu pedido de baraja de cartas.", "intro": "Este formulario será para registrar oficialmente tu pedido de baraja de cartas.",

View File

@@ -69,6 +69,8 @@
"email_label": "邮箱", "email_label": "邮箱",
"message_label": "留言", "message_label": "留言",
"color_label": "颜色", "color_label": "颜色",
"custom_color_label": "自定义颜色",
"custom_color_note": "我会用与您所选颜色最接近的颜色来绘制",
"shipping_label": "配送", "shipping_label": "配送",
"colors": { "colors": {
"red": "红色", "red": "红色",
@@ -82,6 +84,14 @@
"black": "黑色", "black": "黑色",
"white": "白色", "white": "白色",
"pink": "粉色", "pink": "粉色",
"turquoise": "青绿色",
"teal": "蓝绿色",
"navy": "藏青色",
"lavender": "淡紫色",
"magenta": "洋红色",
"lime": "黄绿色",
"coral": "珊瑚色",
"custom": "自定义",
"none": "无" "none": "无"
}, },
"submit": "提交订单", "submit": "提交订单",
@@ -89,6 +99,7 @@
"success": "订单提交成功。", "success": "订单提交成功。",
"error_required": "所有字段均为必填。", "error_required": "所有字段均为必填。",
"error_invalid": "部分数据无效,请检查表单后重试。", "error_invalid": "部分数据无效,请检查表单后重试。",
"error_invalid_color": "自定义颜色无效。",
"error_generic": "无法提交订单,请稍后再试。", "error_generic": "无法提交订单,请稍后再试。",
"form_title": "纸牌订购表单", "form_title": "纸牌订购表单",
"intro": "此表单用于正式登记您的纸牌订单。", "intro": "此表单用于正式登记您的纸牌订单。",