Ranges display with dynamic color and animations

This commit is contained in:
Jérémy Munsch 2024-01-15 17:53:16 +01:00
parent 78cb1b609a
commit ed37de4cfa
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
12 changed files with 208 additions and 9 deletions

View file

@ -37,6 +37,7 @@
@import "bootstrap/scss/modal";
@import "bootstrap/scss/accordion";
@import "bootstrap/scss/toasts";
@import "bootstrap/scss/badge";
// 6. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
@import "bootstrap/scss/utilities/api";

View file

@ -1,6 +1,8 @@
@import "./variables";
@import "./bootstrap";
@import "./sweetalert-custom";
@import 'animate.css';
// * App
@import "./layout";

View file

@ -4,9 +4,6 @@ export default class AppState {
/** Json data as exported from zip and compliant with schemas */
private jsonData: TypeApp;
/** The selected brand */
private selectedBrand: TypeBrand | null = null;
constructor(jsonData: TypeApp) {
this.jsonData = jsonData;
}
@ -47,7 +44,7 @@ export default class AppState {
}
return brandItem;
});
return ranges;
return ranges.sort((rangeA, rangeB) => rangeA.order - rangeB.order);
}
public range(slug:string): TypeRange|undefined {

33
classes/ToolboxHelper.ts Normal file
View file

@ -0,0 +1,33 @@
export default class ToolboxHelper {
/**
* Get the contrast for any color
*
* @param hexcolor An HTML Hex color
* @returns
*/
public static getContrastYIQ(hexcolor: string): "#000000"|"#FFFFFF" {
const r = parseInt(hexcolor.substring(1,3),16);
const g = parseInt(hexcolor.substring(3,5),16);
const b = parseInt(hexcolor.substring(5,7),16);
const yiq = ((r*299)+(g*587)+(b*114))/1000;
return (yiq >= 128) ? "#000000" : "#FFFFFF";
}
/**
* Convert HTML Hex color to RGBA
*
* @param hexcolor An HTML Hex color
* @param alpha The transparency to apply
* @returns
*/
public static toRGBA(hexcolor: string, alpha: number = 1): string {
const r = parseInt(hexcolor.substring(1,3),16);
const g = parseInt(hexcolor.substring(3,5),16);
const b = parseInt(hexcolor.substring(5,7),16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
public static randomInteger(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

22
index.d.ts vendored
View file

@ -29,9 +29,21 @@ type SettingsModel = {
updated_at: string;
};
/** A font from Showroom Bo */
type FontModel = {
/** The unique identifier for a font */
id: number;
/** Nom de la marque */
font_name: string;
/** Chemin vers fichier de la font, chemin tel que présent dans le zip*/
font: string;
created_at: string;
updated_at: string;
};
/** A brand from Showroom Bo */
type BrandModel = {
/** The unique identifier for a product */
/** The unique identifier for a brand */
id: number;
/** Nom de la marque */
name: string;
@ -56,8 +68,16 @@ type RangeModel = {
slug: string;
/** Logo de la gamme, chemin tel que présent dans le zip */
picture: string;
/** Couleur de la gamme */
color: string;
/** Largeur de la gamme 1 colonne ou 2 colonnes (pleine largeur)e */
width: number;
/** Ordre d'affichage de la gamme */
order: number;
/** Clé étrangère de la marque */
brand_id: number;
/** Clé étrangère de la police */
font_id: number;
created_at: string;
updated_at: string;
products: Array<ProductModel>;

View file

@ -54,6 +54,7 @@ export default defineNuxtConfig({
// Global CSS: https://go.nuxtjs.dev/config-css
css: ["~/assets/scss/style.scss", "@fortawesome/fontawesome-svg-core/styles.css"],
plugins: [
{ src: "~/plugins/toolboxhelper.ts", mode: "client" },
{ src: "~/plugins/boostrap.ts", mode: "client" },
{ src: "~/plugins/fontawesome.ts", mode: "client" },
{ src: "~/plugins/localStorage.ts", mode: "client" },

7
package-lock.json generated
View file

@ -25,6 +25,7 @@
"adm-zip": "^0.5.10",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"dompurify": "^3.0.6",
"electron": "^28.1.1",
@ -5314,6 +5315,12 @@
}
}
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"dev": true
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",

View file

@ -40,6 +40,7 @@
"adm-zip": "^0.5.10",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"dompurify": "^3.0.6",
"electron": "^28.1.1",

View file

@ -1,8 +1,15 @@
<template>
<div class="page-brands-show">
<div class="container">
<div v-if="brand.ranges" class="row">
<div v-for="(range, rangeIndex) of brand.ranges" :key="rangeIndex" class="col-12 col-md-6">
<div v-if="brand.ranges" class="row justify-content-center">
<div
v-for="(range, rangeIndex) of brand.ranges"
:key="rangeIndex"
:class="{
'col-12 text-center my-5 py-5': true,
'col-md-6': range.width === 1
}"
>
<button
:class="{
'd-inline-block btn position-relative': true,
@ -14,7 +21,22 @@
<div>
<img class="img-fluid" :src="`/data/${range.picture}`" :alt="`Gamme ${range.name}`">
</div>
<span class="fs-2 fw-bold">{{ range.name }}</span>
<span
:class="{
...{
'range-title fs-2 fw-bold mt-5 d-inline-block px-5 py-3 rounded text-capitalize animate__animated': true,
'text-dark': getContrastYIQ(range.color) === '#000000',
'text-light': getContrastYIQ(range.color) === '#FFFFFF',
},
...titleAnimations(rangeIndex)
}"
:style="{
'background-color': range.color,
'text-shadow': `0 0 0.2rem ${toRGBA(getContrastYIQ(range.color), 1)}`,
}"
>
{{ range.name }}
</span>
</button>
</div>
</div>
@ -32,6 +54,7 @@ import { defineComponent } from "vue";
import { useRoute, type RouteLocationNormalizedLoaded } from "vue-router";
import type AppState from "~/classes/AppState";
import Toast, { ToastTypeEnum } from "~/classes/Toast";
import ToolboxHelper from "~/classes/ToolboxHelper";
import { useAppStore } from "~/stores/useAppStore";
import bsTooltips from "~/vueModules/bsTooltips";
@ -41,11 +64,17 @@ export default defineComponent({
appState: AppState;
route: RouteLocationNormalizedLoaded;
brand: TypeBrand;
animateTimeout: NodeJS.Timeout|undefined;
animateIndex: number|null;
animations: Array<string>;
} {
return {
appState: {} as AppState,
route: useRoute(),
brand: {} as TypeBrand
brand: {} as TypeBrand,
animateTimeout: undefined,
animateIndex: null,
animations: ["heartBeat", "tada", "wooble", "rubberBand"]
};
},
mounted() {
@ -58,8 +87,42 @@ export default defineComponent({
return ;
}
this.brand = brand;
// * ANIMATIONS
clearTimeout(this.animateTimeout);
this.$nextTick(() => {
[...document.getElementsByClassName("range-title")].forEach(el => {
el.classList.add("animate__bounce");
});
this.animateTimeout = setTimeout(() => {
[...document.getElementsByClassName("range-title")].forEach(el => {
el.classList.remove("animate__bounce");
});
this.setRangeToAnimate();
}, ToolboxHelper.randomInteger(4000, 8000));
});
},
beforeUnmount() {
clearTimeout(this.animateTimeout);
},
methods: {
titleAnimations(rangeIndex: number) {
const animation = this.animations.at(ToolboxHelper.randomInteger(0, this.animations.length -1));
const out: Map<string, boolean> = new Map<string, boolean>();
out.set(`animate__${animation}`, this.animateIndex === rangeIndex);
return Object.fromEntries(out.entries());
},
setRangeToAnimate(): void {
clearTimeout(this.animateTimeout);
this.animateIndex = Math.ceil(Math.random() * this.brand.ranges.length) -1;
this.animateTimeout = setTimeout(this.setRangeToAnimate, ToolboxHelper.randomInteger(2000, 6000));
},
getContrastYIQ(hexcolor: string): string {
return ToolboxHelper.getContrastYIQ(hexcolor);
},
toRGBA(hexcolor: string, alpha: number = 1): string {
return ToolboxHelper.toRGBA(hexcolor, alpha);
},
gotoRange(range: TypeRange) {
useAppStore().setActiveRange(range);
navigateTo(`/ranges/${range.slug}`);

9
plugins/toolboxhelper.ts Normal file
View file

@ -0,0 +1,9 @@
import ToolboxHelper from "./../classes/ToolboxHelper";
export default defineNuxtPlugin(() => {
return {
provide: {
ToolboxHelper
}
};
});

37
schemas/font.schema.json Normal file
View file

@ -0,0 +1,37 @@
{
"$id": "schemas/font.schema.json",
"title": "Font",
"description": "A font from Showroom Bo",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a font",
"type": "integer",
"minimum": 1
},
"font_name": {
"description": "Nom de la font",
"type": "string"
},
"font": {
"description": "Chemin vers fichier de la font, chemin tel que présent dans le zip",
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"font_name",
"font",
"created_at",
"updated_at",
"ranges"
]
}

View file

@ -21,11 +21,31 @@
"description": "Logo de la gamme, chemin tel que présent dans le zip",
"type": "string"
},
"color": {
"description": "Couleur de la gamme",
"$ref": "#/$defs/hexcolor"
},
"width": {
"description": "Largeur de la gamme 1 colonne ou 2 colonnes (pleine largeur)",
"type": "integer",
"minimum": 1,
"maximum": 2
},
"order": {
"description": "Ordre d'affichage de la gamme",
"type": "integer",
"minimum": 1
},
"brand_id": {
"description": "Clé étrangère de la marque",
"type": "integer",
"minimum": 1
},
"font_id": {
"description": "Clé étrangère de la police",
"type": "integer",
"minimum": 1
},
"created_at": {
"type": "string",
"format": "date-time"
@ -41,12 +61,20 @@
}
}
},
"$defs": {
"hexcolor": {
"description": "HEX color",
"pattern": "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$",
"type": "string"
}
},
"required": [
"id",
"name",
"slug",
"picture",
"brand_id",
"font_id",
"created_at",
"updated_at",
"products"