Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
4f73e0068b
commit
992665f4ea
39 changed files with 4331 additions and 36 deletions
|
|
@ -40,6 +40,165 @@
|
|||
"id": "m9",
|
||||
"type": "timer"
|
||||
},
|
||||
{
|
||||
"id": "md",
|
||||
"type": "calendar",
|
||||
"title": "Calendrier hebdomadaire",
|
||||
"controls": [
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "calendar",
|
||||
"title": "Bouton changement de couleur",
|
||||
"preview": {
|
||||
"kind": "color"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "refresh",
|
||||
"title": "Répéter les événements",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "refresh",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "eye-closed",
|
||||
"title": "Masquer / Afficher les heures inutiles",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "eye-closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-mute",
|
||||
"title": "Alerte désactivée",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-mute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-min",
|
||||
"title": "Alerte sur la page",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-min"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte sur le site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "trash",
|
||||
"title": "Bouton supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Exemple texte du calendrier hebdomadaire de démonstration.",
|
||||
"example": "@viewMode: week\nevent | title=Uturi | day=lundi | start=00:15 | end=00:20\nevent | title=Uturi | day=lundi | start=16:00 | end=16:05\nevent | title=Uturi | day=mardi | start=19:00 | end=19:05\nevent | title=Uturi | day=mercredi | start=22:15 | end=22:20\nevent | title=Uturi | day=jeudi | start=00:15 | end=00:20\nevent | title=Uturi | day=vendredi | start=12:00 | end=12:05\nevent | title=Uturi | day=samedi | start=02:00 | end=02:05\nevent | title=Uturi | day=dimanche | start=12:00 | end=12:05\nevent | title=Garmoth | color=pink | day=lundi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=mardi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=mercredi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=jeudi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=vendredi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=samedi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=dimanche | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=lundi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=mardi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=mercredi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=jeudi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=vendredi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=dimanche | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=dimanche | start=19:15 | end=19:20\nevent | title=Vell | color=cyan | day=mercredi | start=19:00 | end=19:05\nevent | title=Vell | color=cyan | day=dimanche | start=16:00 | end=16:05\nevent | title=Maintenance | color=gold | day=jeudi | start=10:00 | end=14:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "me",
|
||||
"type": "calendar",
|
||||
"title": "Calendrier mensuel",
|
||||
"controls": [
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "calendar",
|
||||
"title": "Bouton changement de couleur",
|
||||
"preview": {
|
||||
"kind": "color"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "day-number",
|
||||
"title": "Numéro de jour",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "day-number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "day-name",
|
||||
"title": "Nom de jour",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "day-name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "refresh",
|
||||
"title": "Répéter les événements",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "refresh",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-mute",
|
||||
"title": "Alerte désactivée",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-mute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-min",
|
||||
"title": "Alerte sur la page",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-min"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte sur le site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "trash",
|
||||
"title": "Bouton supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Exemple texte du calendrier mensuel de démonstration.",
|
||||
"example": "@viewMode: month\nevent | title=Double EXP 1er Week end du mois | repeatBy=weekday | repeat=none | startDate=2026-08-01 | endDate=2026-08-02\nevent | title=Event de drop rare | color=gold | repeatBy=weekday | repeat=none | startDate=2026-08-18 | endDate=2026-08-25\nevent | title=Maintenance | color=pink | repeatBy=weekday | repeat=weekly | startDate=2026-08-04\nevent | title=Récompenses du 15 | color=green | repeatBy=monthDay | repeat=none | startDate=2026-08-15"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ma",
|
||||
"type": "taskPlanner"
|
||||
|
|
@ -54,8 +213,8 @@
|
|||
}
|
||||
],
|
||||
"moduleOrder": {
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6"], ["m7", "m8", "m9", "ma", "mb", "mc"]]
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "md", "me", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6", "ma"], ["m7", "m8", "m9", "md", "me", "mb", "mc"]]
|
||||
},
|
||||
"updatedAt": "2026-08-09T07:34:31.425Z",
|
||||
"moduleColumns": 1
|
||||
|
|
@ -1325,6 +1484,238 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"md": {
|
||||
"initialized": true,
|
||||
"events": [
|
||||
{
|
||||
"id": "hdl7oex",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 15,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 20
|
||||
},
|
||||
{
|
||||
"id": "hgdodzy",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 960,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 965
|
||||
},
|
||||
{
|
||||
"id": "hy95mcm",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 1140,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 1145
|
||||
},
|
||||
{
|
||||
"id": "hs2tg2h",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 1335,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1340
|
||||
},
|
||||
{
|
||||
"id": "hv903h8",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 15,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 20
|
||||
},
|
||||
{
|
||||
"id": "hanpqlt",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 720,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 725
|
||||
},
|
||||
{
|
||||
"id": "h0e6ma2",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 120,
|
||||
"dayIndex": 5,
|
||||
"endMinutes": 125
|
||||
},
|
||||
{
|
||||
"id": "hc3ocs0",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 720,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 725
|
||||
},
|
||||
{
|
||||
"id": "hv1i4lf",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h1uz1u2",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hdci41f",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h08ha51",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hnz11qv",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h2dexvq",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 5,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hq9ctue",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hfq7scf",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hfbeg1y",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h2gfv4o",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hjjzqfz",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h1h2zsd",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "ht08lsw",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h8k8cbm",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1155,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 1160,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hzq4r4v",
|
||||
"title": "Vell",
|
||||
"startMinutes": 1140,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1145,
|
||||
"color": "cyan"
|
||||
},
|
||||
{
|
||||
"id": "hxq7o9p",
|
||||
"title": "Vell",
|
||||
"startMinutes": 960,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 965,
|
||||
"color": "cyan"
|
||||
},
|
||||
{
|
||||
"id": "hfkycds",
|
||||
"title": "Maintenance",
|
||||
"startMinutes": 600,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 840,
|
||||
"color": "gold"
|
||||
}
|
||||
]
|
||||
},
|
||||
"me": {
|
||||
"initialized": true,
|
||||
"viewMode": "month",
|
||||
"events": [
|
||||
{
|
||||
"id": "hpnato7",
|
||||
"title": "Double EXP 1er Week end du mois",
|
||||
"startDate": "2026-08-01",
|
||||
"endDate": "2026-08-02",
|
||||
"monthlyRepeatBy": "weekday"
|
||||
},
|
||||
{
|
||||
"id": "hdygtpx",
|
||||
"title": "Event de drop rare",
|
||||
"startDate": "2026-08-18",
|
||||
"endDate": "2026-08-25",
|
||||
"monthlyRepeatBy": "weekday",
|
||||
"color": "gold"
|
||||
},
|
||||
{
|
||||
"id": "hy8bjtd",
|
||||
"title": "Maintenance",
|
||||
"startDate": "2026-08-04",
|
||||
"repeat": "weekly",
|
||||
"monthlyRepeatBy": "weekday",
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hytouda",
|
||||
"title": "Récompenses du 15",
|
||||
"startDate": "2026-08-15",
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ma": {
|
||||
"lastResetAt": 1786226400000,
|
||||
"hideCompleted": true,
|
||||
|
|
|
|||
|
|
@ -884,6 +884,90 @@
|
|||
"example": "@activeTab: countdown\nlap | label=Fosse 110 | elapsedMs=1046475\nlap | label=Fosse 55 | elapsedMs=50019\ncountdown | label=Fin de vague infernale | type=time_pattern | alertMode=off | pattern=X:55:X\ncountdown | label=Boss Mondial | type=daily_time | alertMode=off | time=14:00:00\ncountdown | label=Legion | type=interval | alertMode=off | autoRefresh=true | intervalMs=1800000 | startMode=now\ncountdown | label=Durée | type=duration | alertMode=off | durationMs=1800000"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"controls": [
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "calendar",
|
||||
"title": "Sélection de créneaux",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "calendar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "copy",
|
||||
"title": "Copier un événement",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Alertes",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "trash",
|
||||
"title": "Supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
{
|
||||
"title": "Vue hebdomadaire",
|
||||
"text": "Une grille jours par créneaux de 30 minutes pour planifier les événements de session.",
|
||||
"details": [
|
||||
"Les heures peuvent être saisies à 5 minutes près.",
|
||||
"La grille reste lisible dans la toolbox complète et dans le panneau latéral.",
|
||||
"Les plages vides peuvent être masquées sans retirer l'indicateur de tranche horaire."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vue mensuelle",
|
||||
"text": "Une grille semaines par jours affiche les numéros du mois courant sans nommer le mois.",
|
||||
"details": [
|
||||
"Les événements mensuels peuvent cibler un numéro du jour ou un jour de semaine.",
|
||||
"Les événements multi-jours s'affichent en barre horizontale.",
|
||||
"Les jours hors mois courant ne sont pas sélectionnables."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Création rapide",
|
||||
"text": "Un événement peut être saisi au formulaire ou prérempli en sélectionnant des cellules.",
|
||||
"details": [
|
||||
"Un événement peut être ponctuel ou avoir une durée.",
|
||||
"Dans un calendrier hebdomadaire, les événements se répètent automatiquement chaque semaine.",
|
||||
"Un événement copié peut être collé sur un autre créneau en conservant durée, couleur et alerte.",
|
||||
"La couleur permet de distinguer rapidement les catégories d'événements."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Alertes",
|
||||
"text": "Les alertes utilisent les mêmes modes que le timer, avec un délai de 5 ou 10 minutes.",
|
||||
"details": [
|
||||
"Aucune alerte garde l'événement silencieux.",
|
||||
"Alerte visible prévient seulement quand la toolbox concernée est ouverte.",
|
||||
"Alerte site fonctionne sur toute l'application et signale aussi une session déjà en cours une seule fois."
|
||||
]
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Le calendrier s'échange en lignes texte : une meta @viewMode puis une ligne event par entrée.",
|
||||
"example": "@viewMode: week\nevent | title=Raid hebdo | day=vendredi | start=18:00 | end=20:00 | color=gold | alertMode=site | alerts=10,5\n\n@viewMode: month\nevent | title=Week-end Double EXP | repeatBy=monthDay | repeat=monthly | startDate=2026-08-07 | endDate=2026-08-09\nevent | title=Maintenance | repeatBy=weekday | repeat=weekly | startDate=2026-08-06 | color=pink"
|
||||
}
|
||||
},
|
||||
"taskPlanner": {
|
||||
"controls": [
|
||||
{
|
||||
|
|
@ -1792,6 +1876,81 @@
|
|||
"renameTitle": "Renommer",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"calendar": {
|
||||
"init": {
|
||||
"title": "Créer un calendrier",
|
||||
"weekly": "Hebdomadaire",
|
||||
"weeklyDescription": "Grille jours par créneaux de 30 minutes.",
|
||||
"monthly": "Mensuel",
|
||||
"monthlyDescription": "Grille mensuelle datée avec navigation.",
|
||||
"soon": "À venir"
|
||||
},
|
||||
"gridLabel": "Calendrier hebdomadaire",
|
||||
"monthGridLabel": "Calendrier mensuel",
|
||||
"titleLabel": "Titre",
|
||||
"titlePlaceholder": "Raid, reset, event...",
|
||||
"dayLabel": "Jour",
|
||||
"startDayLabel": "Date début",
|
||||
"endDayLabel": "Date fin",
|
||||
"startLabel": "Début",
|
||||
"endLabel": "Fin",
|
||||
"alertModeTitle": "Mode d'alerte",
|
||||
"alertTypeLabel": "Type d'alertes",
|
||||
"alertOffTitle": "Pas d'alerte",
|
||||
"alertVisibleTitle": "Alerte quand la toolbox est affichée",
|
||||
"alertSiteTitle": "Alerte sur tout le site",
|
||||
"alertOffsetLabel": "Alerte",
|
||||
"alertOffset10": "10 min avant",
|
||||
"alertOffset5": "5 min avant",
|
||||
"alertOffset0": "Au début et pendant",
|
||||
"alertMessage": "{label} commence bientôt dans {toolbox}.",
|
||||
"resumeAlertMessage": "{label} est en cours dans {toolbox}.",
|
||||
"hideEmptyHoursLabel": "Masquer les heures vides",
|
||||
"colorLabel": "Couleur",
|
||||
"colorViolet": "Violet",
|
||||
"colorGold": "Or",
|
||||
"colorCyan": "Cyan",
|
||||
"colorPink": "Rose",
|
||||
"colorGreen": "Vert",
|
||||
"colorGray": "Gris",
|
||||
"repeatableTitle": "Répétable",
|
||||
"repeatLabel": "Répétition",
|
||||
"repeatNone": "Aucune",
|
||||
"repeatWeekly": "Chaque semaine",
|
||||
"repeatMonthly": "Chaque mois",
|
||||
"monthlyRepeatLabel": "Règle mensuelle",
|
||||
"monthlyRepeatMonthDay": "Numéro du jour",
|
||||
"monthlyRepeatWeekday": "Jour de semaine",
|
||||
"monthDatePickerLabel": "Choisir une date",
|
||||
"previousMonthTitle": "Mois précédent",
|
||||
"nextMonthTitle": "Mois suivant",
|
||||
"currentMonthTitle": "Mois courant",
|
||||
"previousShortLabel": "Prec.",
|
||||
"nextShortLabel": "Suiv.",
|
||||
"importPlaceholder": "@viewMode: week\nevent | title=Raid hebdo | day=vendredi | start=18:00 | end=20:00 | color=gold | alertMode=site | alerts=10,5\n\n@viewMode: month\nevent | title=Maintenance | repeatBy=weekday | repeat=monthly | startDate=2026-08-03 | endDate=2026-08-05 | color=pink",
|
||||
"importOpenButton": "Importer du texte",
|
||||
"exportOpenButton": "Exporter en format texte",
|
||||
"importModalTitle": "Importer un calendrier",
|
||||
"exportModalTitle": "Exporter un calendrier",
|
||||
"importButton": "Importer le texte",
|
||||
"exportButton": "Copier le texte",
|
||||
"exportInfo": "Format calendrier texte compatible avec les vues hebdomadaire et mensuelle.",
|
||||
"importSuccess": "Import calendrier terminé.",
|
||||
"exportSuccess": "Export calendrier copié.",
|
||||
"importError": "Format de calendrier invalide.",
|
||||
"addButton": "Ajouter",
|
||||
"saveButton": "Enregistrer",
|
||||
"cancelButton": "Annuler",
|
||||
"editTitle": "Modifier",
|
||||
"copyTitle": "Copier",
|
||||
"copiedTitle": "Événement copié.",
|
||||
"pastedTitle": "Événement collé.",
|
||||
"deleteTitle": "Supprimer",
|
||||
"deleteOccurrenceTitle": "Supprimer cette occurrence",
|
||||
"deleteSeriesTitle": "Supprimer toutes les répétitions",
|
||||
"requiredError": "Titre, date et heure de début requis.",
|
||||
"invalidRangeError": "La fin doit être après le début."
|
||||
},
|
||||
"taskPlanner": {
|
||||
"importPlaceholder": "@resetTime: 06:00\n# Raid\n- daily Préparer la route\n @description: Notes rapides\n - unique Sous-tâche",
|
||||
"importOpenButton": "Importer du texte",
|
||||
|
|
|
|||
96
website/public/static/icons/calendar-month.svg
Normal file
96
website/public/static/icons/calendar-month.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="m 8.2553905,10.353164 c -0.4995098,0 -0.992121,0.05377 -1.4778512,0.161309 -0.4857302,0.104074 -0.8698233,0.21335 -1.1523047,0.327831 v 1.680821 h 0.1911912 c 0.3548241,-0.211618 0.7216867,-0.372913 1.1006252,-0.483926 0.3789384,-0.114481 0.7303398,-0.171738 1.0541599,-0.171738 0.130906,0 0.2876355,0.01042 0.4702149,0.03123 0.1825795,0.02081 0.3410204,0.06242 0.4753713,0.124863 0.1205713,0.05551 0.2204744,0.131848 0.2997069,0.228984 0.079233,0.09367 0.1188864,0.218537 0.1188864,0.374648 0,0.211618 -0.037942,0.372972 -0.11373,0.483985 -0.075788,0.107543 -0.1894406,0.194242 -0.341016,0.260156 -0.1446856,0.06245 -0.320357,0.102361 -0.5270507,0.119707 -0.2066937,0.01735 -0.4444427,0.02602 -0.7131446,0.02602 H 7.2012305 v 1.38416 h 0.4237502 c 0.2032488,0 0.4237083,0.0035 0.6614061,0.01043 0.2376978,0.0035 0.4409267,0.03297 0.6097266,0.08848 0.1687998,0.05204 0.3048759,0.140482 0.4082228,0.265371 0.1033468,0.12142 0.155039,0.294877 0.155039,0.520371 0,0.232433 -0.037885,0.411127 -0.1136722,0.536016 -0.075788,0.12142 -0.1774021,0.225459 -0.3048632,0.312188 -0.1619101,0.111013 -0.3600048,0.183863 -0.5942577,0.218554 -0.2342528,0.03122 -0.45129,0.04682 -0.6510939,0.04682 -0.3410446,0 -0.7182336,-0.06587 -1.131621,-0.197696 C 6.2539247,16.569911 5.9008119,16.413794 5.6045509,16.233398 H 5.4030466 v 1.701621 c 0.2859263,0.12489 0.6562117,0.237598 1.1109379,0.338203 0.458171,0.100606 1.0058972,0.150938 1.6432028,0.150938 0.5580729,0 1.0317427,-0.05726 1.4210158,-0.171738 0.3927181,-0.117951 0.7337929,-0.28617 1.0231639,-0.504727 0.296261,-0.225494 0.515009,-0.496124 0.65625,-0.811816 0.144686,-0.315693 0.217032,-0.655681 0.217032,-1.019942 0,-0.32263 -0.04998,-0.58104 -0.149884,-0.775312 -0.0999,-0.194272 -0.234207,-0.364296 -0.403007,-0.51 -0.16191,-0.138766 -0.341063,-0.24455 -0.537422,-0.317402 -0.196359,-0.07285 -0.382357,-0.121439 -0.558047,-0.145723 v -0.07283 c 0.434057,-0.09367 0.783688,-0.308786 1.048945,-0.645293 0.265257,-0.339977 0.39791,-0.72506 0.39791,-1.155234 0,-0.3261 -0.06892,-0.610519 -0.206718,-0.85336 -0.137796,-0.24284 -0.347929,-0.449289 -0.63041,-0.619277 C 10.198318,10.679269 9.8968973,10.56656 9.5317384,10.483301 9.1665795,10.396572 8.7411207,10.353164 8.2553905,10.353164 Z m 7.1558795,0.140508 c -0.01378,0.208149 -0.06547,0.381605 -0.155039,0.520371 -0.08957,0.138766 -0.223932,0.251533 -0.403067,0.338262 -0.151575,0.07285 -0.340996,0.124871 -0.56836,0.156094 -0.227363,0.02775 -0.465053,0.04166 -0.713085,0.04166 v 1.264454 h 1.62252 v 4.095351 h -1.62252 v 1.352989 h 5.136328 v -1.35299 H 17.12168 v -6.416191 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
96
website/public/static/icons/calendar-week.svg
Normal file
96
website/public/static/icons/calendar-week.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 9.2709372,10.640683 V 12.12375 H 13.223965 L 9.6068553,18.389004 H 11.818477 L 15.30123,12.180996 V 10.640683 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
4
website/public/static/icons/calendar.svg
Normal file
4
website/public/static/icons/calendar.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9H21M7 3V5M17 3V5M6 12H8M11 12H13M16 12H18M6 15H8M11 15H13M16 15H18M6 18H8M11 18H13M16 18H18M6.2 21H17.8C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4802 21 18.9201 21 17.8V8.2C21 7.07989 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V17.8C3 18.9201 3 19.4802 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 864 B |
60
website/public/static/icons/day-name.svg
Normal file
60
website/public/static/icons/day-name.svg
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:0.750001;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 4.5591795,10.984394 V 17.79293 H 8.2763086 V 16.476035 H 5.888203 v -5.491641 z m 4.4398829,0 v 4.348536 c 0,0.838304 0.1926982,1.481497 0.5780276,1.929609 0.3876367,0.448111 0.958715,0.672129 1.713222,0.672129 0.754507,0 1.325586,-0.224018 1.713223,-0.672129 0.387637,-0.448112 0.581426,-1.092828 0.581426,-1.93418 v -4.343965 h -1.335937 v 4.24793 c 0,0.490789 -0.07732,0.847449 -0.231915,1.069981 -0.152285,0.222531 -0.394537,0.333808 -0.726797,0.333808 -0.336874,0 -0.580334,-0.115843 -0.730312,-0.347519 -0.149978,-0.231677 -0.224941,-0.583771 -0.224941,-1.05627 v -4.24793 z m 5.8243366,0 v 6.808536 h 1.218281 v -4.673145 l 2.187421,4.673145 h 1.280567 v -6.808536 h -1.218281 v 3.900411 l -1.879336,-3.900411 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
96
website/public/static/icons/day-number.svg
Normal file
96
website/public/static/icons/day-number.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 9.2709372,10.640683 V 12.12375 H 13.223965 L 9.6068553,18.389004 H 11.818477 L 15.30123,12.180996 V 10.640683 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
40
website/public/static/icons/trashcan-one.svg
Normal file
40
website/public/static/icons/trashcan-one.svg
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath5">
|
||||
<path
|
||||
id="path5"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.95705;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 0.21029297,0.25236328 V 23.805059 H 23.636836 V 0.25236328 Z M 16.530645,9.2983593 a 7.0619658,7.1584014 0 0 1 7.061953,7.1583987 7.0619658,7.1584014 0 0 1 -7.061953,7.158398 7.0619658,7.1584014 0 0 1 -7.0619535,-7.158398 7.0619658,7.1584014 0 0 1 7.0619535,-7.1583987 z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M 4,6 H 20 M 16,6 15.7294,5.18807 C 15.4671,4.40125 15.3359,4.00784 15.0927,3.71698 14.8779,3.46013 14.6021,3.26132 14.2905,3.13878 13.9376,3 13.523,3 12.6936,3 H 11.3064 C 10.477,3 10.0624,3 9.70951,3.13878 9.39792,3.26132 9.12208,3.46013 8.90729,3.71698 8.66405,4.00784 8.53292,4.40125 8.27064,5.18807 L 8,6 m 10,0 v 10.2 c 0,1.6802 0,2.5202 -0.327,3.162 -0.2876,0.5645 -0.7465,1.0234 -1.311,1.311 C 15.7202,21 14.8802,21 13.2,21 H 10.8 C 9.11984,21 8.27976,21 7.63803,20.673 7.07354,20.3854 6.6146,19.9265 6.32698,19.362 6,18.7202 6,17.8802 6,16.2 V 6 m 8,4 v 7 m -4,-7 v 7"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1"
|
||||
clip-path="url(#clipPath5)" />
|
||||
<path
|
||||
id="path3"
|
||||
style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:2.05989;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="m 16.299316,10.251504 a 6.08775,6.2262435 0 0 0 -6.087714,6.226289 6.08775,6.2262435 0 0 0 6.087714,6.22623 6.08775,6.2262435 0 0 0 6.087774,-6.22623 6.08775,6.2262435 0 0 0 -6.087774,-6.226289 z" />
|
||||
<path
|
||||
id="text6"
|
||||
style="font-weight:bold;font-size:9.34525px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.00535405px;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.92177;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;paint-order:stroke fill markers"
|
||||
d="m 15.813443,12.788217 c -0.01236,0.188228 -0.05871,0.345068 -0.139028,0.470552 -0.08033,0.125487 -0.200793,0.227424 -0.361435,0.305853 -0.135927,0.06588 -0.305856,0.112981 -0.509746,0.141216 -0.203892,0.0251 -0.417006,0.03761 -0.639432,0.03761 v 1.143479 h 1.455026 v 3.703385 h -1.455026 v 1.223536 h 4.606055 v -1.223536 h -1.42259 v -5.8021 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -78,6 +78,7 @@ export function AppOverlays({
|
|||
key={notification.id}
|
||||
id={notification.id}
|
||||
message={notification.message}
|
||||
durationMs={notification.durationMs}
|
||||
onClose={dismissNotification}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance
|
|||
);
|
||||
}
|
||||
|
||||
export function NotificationToast({ id, message, onClose }) {
|
||||
export function NotificationToast({ id, message, durationMs = 120000, onClose }) {
|
||||
useEffect(() => {
|
||||
const timeoutId = window.setTimeout(() => onClose(id), 120000);
|
||||
const timeoutId = window.setTimeout(() => onClose(id), durationMs);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [id, onClose]);
|
||||
}, [durationMs, id, onClose]);
|
||||
|
||||
return (
|
||||
<div className="notification-toast" role="status">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
|
|
@ -191,6 +192,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
|
|
@ -220,7 +222,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
setShowTutorial(shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
||||
}, [pendingTutorialToolboxId, toolbox, toolboxId, toolboxTutorial]);
|
||||
|
||||
function addModule(type) {
|
||||
function addModule(type, initialData = null) {
|
||||
if (!type) return;
|
||||
const module = { id: uid("mod"), type };
|
||||
const modules = [...toolbox.modules, module];
|
||||
|
|
@ -229,6 +231,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
modules,
|
||||
moduleOrder: addModuleToOrder(toolbox.moduleOrder, modules, module.id)
|
||||
});
|
||||
if (initialData) updateModuleData(toolbox.id, module.id, initialData, type);
|
||||
}
|
||||
|
||||
function createImageAnnotationModule(dataUrl) {
|
||||
|
|
|
|||
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -442,9 +442,6 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
<div className="combos-module">
|
||||
{(editing || editingComboId) && (
|
||||
<div className="module-add-panel combos-editor">
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
<div className="combos-device-row">
|
||||
<label>
|
||||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
||||
|
|
@ -592,6 +589,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
// Rôle : regroupe les calculs hebdomadaires/mensuels, occurrences et alertes du calendrier.
|
||||
export const CALENDAR_VIEW_MODES = ["week", "month"];
|
||||
export const CALENDAR_MONTHLY_REPEAT_BY = ["monthDay", "weekday"];
|
||||
export const CALENDAR_REPEAT_MODES = ["none", "daily", "weekly", "monthly"];
|
||||
export const CALENDAR_MONTH_REPEAT_MODES = ["none", "weekly", "monthly"];
|
||||
export const CALENDAR_ALERT_MODES = ["off", "visible", "site"];
|
||||
export const CALENDAR_ALERT_OFFSETS = [10, 5, 0];
|
||||
export const CALENDAR_COLORS = ["violet", "gold", "cyan", "pink", "green", "gray"];
|
||||
export const WEEK_SLOT_MINUTES = 30;
|
||||
export const WEEK_TIME_INPUT_STEP_MINUTES = 5;
|
||||
export const WEEK_SLOT_COUNT = 48;
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
export const MINUTE_MS = 60 * 1000;
|
||||
export const WEEK_DAY_COUNT = 7;
|
||||
|
||||
export function pad2(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function formatLocalDateKey(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
||||
}
|
||||
|
||||
export function parseLocalDateKey(value) {
|
||||
const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]) - 1;
|
||||
const day = Number(match[3]);
|
||||
const date = new Date(year, month, day);
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
export function minutesToTimeValue(minutes) {
|
||||
const normalized = Math.max(0, Math.min(23 * 60 + 59, Number(minutes) || 0));
|
||||
return `${pad2(Math.floor(normalized / 60))}:${pad2(normalized % 60)}`;
|
||||
}
|
||||
|
||||
export function timeValueToMinutes(value) {
|
||||
const match = String(value || "").match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!match) return -1;
|
||||
const hours = Number(match[1]);
|
||||
const minutes = Number(match[2]);
|
||||
if (!Number.isInteger(hours) || !Number.isInteger(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return -1;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
export function startOfLocalDay(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
export function addDays(dateLike, days) {
|
||||
const date = new Date(dateLike);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMonths(dateLike, months) {
|
||||
const date = new Date(dateLike);
|
||||
date.setMonth(date.getMonth() + months);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getLocalDayIndex(dateLike) {
|
||||
return (new Date(dateLike).getDay() + 6) % WEEK_DAY_COUNT;
|
||||
}
|
||||
|
||||
export function getWeekStart(dateLike, weekStartsOn = 1) {
|
||||
const day = startOfLocalDay(dateLike);
|
||||
const current = day.getDay();
|
||||
const offset = (current - weekStartsOn + 7) % 7;
|
||||
return addDays(day, -offset);
|
||||
}
|
||||
|
||||
export function getWeekDays(weekStart) {
|
||||
return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
|
||||
}
|
||||
|
||||
export function getMonthStart(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
}
|
||||
|
||||
export function getMonthEnd(dateLike) {
|
||||
const date = getMonthStart(dateLike);
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDaysInMonth(dateLike) {
|
||||
return new Date(new Date(dateLike).getFullYear(), new Date(dateLike).getMonth() + 1, 0).getDate();
|
||||
}
|
||||
|
||||
export function getMonthGridDays(dateLike) {
|
||||
const monthStart = getMonthStart(dateLike);
|
||||
const monthEnd = getMonthEnd(dateLike);
|
||||
const gridStart = getWeekStart(monthStart, 1);
|
||||
const gridEnd = addDays(getWeekStart(addDays(monthEnd, -1), 1), 7);
|
||||
const days = [];
|
||||
for (let cursor = new Date(gridStart); cursor < gridEnd; cursor = addDays(cursor, 1)) {
|
||||
const date = new Date(cursor);
|
||||
days.push({
|
||||
date,
|
||||
dayNumber: date.getDate(),
|
||||
dayIndex: getLocalDayIndex(date),
|
||||
inMonth: date.getMonth() === monthStart.getMonth() && date.getFullYear() === monthStart.getFullYear()
|
||||
});
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
export function getDateForWeekDay(weekStart, dayIndex, minutes = 0) {
|
||||
const date = addDays(weekStart, Math.max(0, Math.min(WEEK_DAY_COUNT - 1, Number(dayIndex) || 0)));
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDateForMonthDay(monthDate, monthDay, minutes = 0) {
|
||||
const day = Number(monthDay);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
if (!Number.isInteger(day) || day < 1 || day > getDaysInMonth(monthStart)) return null;
|
||||
const date = new Date(monthStart.getFullYear(), monthStart.getMonth(), day);
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getWeekdayOrdinalInMonth(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return Math.floor((date.getDate() - 1) / 7) + 1;
|
||||
}
|
||||
|
||||
function getMonthlyWeekdayDate(monthDate, sourceDate, minutes = 0) {
|
||||
const source = new Date(sourceDate);
|
||||
const ordinal = getWeekdayOrdinalInMonth(source);
|
||||
const dayIndex = getLocalDayIndex(source);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
const firstDayOffset = (dayIndex - getLocalDayIndex(monthStart) + WEEK_DAY_COUNT) % WEEK_DAY_COUNT;
|
||||
const date = addDays(monthStart, firstDayOffset + (ordinal - 1) * WEEK_DAY_COUNT);
|
||||
if (date.getMonth() !== monthStart.getMonth()) return null;
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getMonthlyDateRange(event, monthDate) {
|
||||
const sourceStart = parseLocalDateKey(event.startDate);
|
||||
const sourceEnd = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!sourceStart || !sourceEnd || sourceEnd < sourceStart) return null;
|
||||
const spanDays = Math.max(0, Math.round((startOfLocalDay(sourceEnd).getTime() - startOfLocalDay(sourceStart).getTime()) / DAY_MS));
|
||||
const start = event.monthlyRepeatBy === "weekday"
|
||||
? getMonthlyWeekdayDate(monthDate, sourceStart, event.startMinutes)
|
||||
: getDateForMonthDay(monthDate, sourceStart.getDate(), event.startMinutes);
|
||||
if (!start) return null;
|
||||
const end = addDays(start, spanDays + 1);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function getDatedEventRange(event) {
|
||||
const start = parseLocalDateKey(event.startDate);
|
||||
const endSource = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!start || !endSource || endSource < start) return null;
|
||||
start.setHours(0, Math.max(0, Number(event.startMinutes) || 0), 0, 0);
|
||||
const end = addDays(endSource, 1);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function getEventDurationMs(event) {
|
||||
const startMinutes = Number(event.startMinutes);
|
||||
const endMinutes = Number(event.endMinutes);
|
||||
return Number.isFinite(startMinutes) && Number.isFinite(endMinutes) && endMinutes > startMinutes ? (endMinutes - startMinutes) * MINUTE_MS : 0;
|
||||
}
|
||||
|
||||
export function getEventEndMs(event, startMs) {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
return durationMs > 0 ? startMs + durationMs : startMs;
|
||||
}
|
||||
|
||||
export function getCalendarAlertOffsets(event) {
|
||||
const rawOffsets = Array.isArray(event?.alertOffsetMinutes) ? event.alertOffsetMinutes : [event?.alertOffsetMinutes];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
function getWeeklyOccurrenceStart(event, weekStart, offset) {
|
||||
if (event.repeat === "none") return getDateForWeekDay(weekStart, event.dayIndex, event.startMinutes);
|
||||
return getDateForWeekDay(addDays(weekStart, offset * 7), event.dayIndex, event.startMinutes);
|
||||
}
|
||||
|
||||
export function getCalendarOccurrences(events, rangeStart, rangeEnd) {
|
||||
const baseWeekStart = getWeekStart(rangeStart);
|
||||
const startMs = rangeStart.getTime();
|
||||
const endMs = rangeEnd.getTime();
|
||||
return events.flatMap((event) => {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
const candidates = [];
|
||||
|
||||
if (event.startDate) {
|
||||
const baseRange = getDatedEventRange(event);
|
||||
if (baseRange) {
|
||||
const spanDays = Math.max(0, Math.round((baseRange.end.getTime() - baseRange.start.getTime()) / DAY_MS) - 1);
|
||||
if (event.repeat === "monthly") {
|
||||
for (let cursor = getMonthStart(rangeStart); cursor < rangeEnd; cursor = addMonths(cursor, 1)) {
|
||||
const range = getMonthlyDateRange(event, cursor);
|
||||
if (range) candidates.push(range);
|
||||
}
|
||||
} else if (event.repeat === "weekly") {
|
||||
const firstOffset = Math.floor((getWeekStart(rangeStart).getTime() - getWeekStart(baseRange.start).getTime()) / (7 * DAY_MS)) - 1;
|
||||
const weekCount = Math.ceil((rangeEnd.getTime() - rangeStart.getTime()) / (7 * DAY_MS)) + 3;
|
||||
for (let index = Math.max(-1, firstOffset); index < firstOffset + weekCount; index += 1) {
|
||||
const start = addDays(baseRange.start, index * 7);
|
||||
const end = addDays(start, spanDays + 1);
|
||||
candidates.push({ start, end });
|
||||
}
|
||||
} else {
|
||||
candidates.push(baseRange);
|
||||
}
|
||||
}
|
||||
} else if (event.repeat === "none") {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, 0));
|
||||
} else {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, -1), getWeeklyOccurrenceStart(event, baseWeekStart, 0), getWeeklyOccurrenceStart(event, baseWeekStart, 1));
|
||||
}
|
||||
|
||||
return candidates
|
||||
.map((candidate) => candidate instanceof Date ? { start: candidate, end: null } : candidate)
|
||||
.filter((candidate) => candidate?.start && !Number.isNaN(candidate.start.getTime()))
|
||||
.filter((candidate) => !event.excludedOccurrences?.includes?.(formatLocalDateKey(candidate.start)))
|
||||
.map((candidate) => {
|
||||
const occurrenceStartMs = candidate.start.getTime();
|
||||
const occurrenceEndMs = candidate.end && !Number.isNaN(candidate.end.getTime()) ? candidate.end.getTime() : 0;
|
||||
return {
|
||||
id: `${event.id}:${occurrenceStartMs}`,
|
||||
event,
|
||||
startMs: occurrenceStartMs,
|
||||
endMs: occurrenceEndMs > occurrenceStartMs ? occurrenceEndMs : durationMs > 0 ? occurrenceStartMs + durationMs : occurrenceStartMs
|
||||
};
|
||||
})
|
||||
.filter((occurrence) => {
|
||||
const visibleEndMs = occurrence.endMs || occurrence.startMs;
|
||||
return visibleEndMs >= startMs && occurrence.startMs < endMs;
|
||||
});
|
||||
}).sort((a, b) => a.startMs - b.startMs);
|
||||
}
|
||||
|
||||
export function getNextCalendarAlertTime(event, nowMs) {
|
||||
if (!event || event.alertMode === "off") return 0;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 370);
|
||||
const alert = getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.flatMap((occurrence) => getCalendarAlertOffsets(event).map((offset) => occurrence.startMs - offset * MINUTE_MS))
|
||||
.filter((alertMs) => alertMs >= nowMs)
|
||||
.sort((a, b) => a - b)[0];
|
||||
return alert || 0;
|
||||
}
|
||||
|
||||
export function getCurrentCalendarOccurrence(event, nowMs) {
|
||||
if (!event?.endMinutes && !event?.startDate) return null;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 1);
|
||||
return getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.find((occurrence) => occurrence.startMs <= nowMs && occurrence.endMs > nowMs) || null;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
|||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { CalendarModule } from "./CalendarModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CombosModule } from "./CombosModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
|
|
@ -28,6 +29,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
|
||||
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
|
||||
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||
calendar: { ...TOOLBOX_MODULE_DEFINITIONS.calendar, Component: CalendarModule },
|
||||
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||
|
|
@ -352,7 +354,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} setEditing={setEditing} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Rôle : parse et sérialise les formats texte/TSV d'échange des outils toolbox.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, minutesToTimeValue, timeValueToMinutes } from "./calendarUtils.js";
|
||||
import { cellAddress, columnIndexToName } from "./tableFormulaEngine.js";
|
||||
import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs } from "./timerUtils.js";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
|||
"calculator",
|
||||
"table",
|
||||
"timer",
|
||||
"calendar",
|
||||
"taskPlanner",
|
||||
"equipmentPlanner",
|
||||
"images",
|
||||
|
|
@ -36,6 +38,12 @@ const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboard
|
|||
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||
const TIMER_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
const CALENDAR_ALERT_MODE_SET = new Set(CALENDAR_ALERT_MODES);
|
||||
const CALENDAR_ALERT_OFFSET_SET = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const CALENDAR_COLOR_SET = new Set(CALENDAR_COLORS);
|
||||
const CALENDAR_MONTHLY_REPEAT_BY_SET = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const CALENDAR_MONTH_REPEAT_MODE_SET = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
const CALENDAR_WEEKDAYS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
|
||||
const IMAGE_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i;
|
||||
|
||||
export function supportsTextExchange(type) {
|
||||
|
|
@ -51,6 +59,7 @@ export function hasTextExchangeContent(type, data) {
|
|||
if (type === "calculator") return Boolean(data?.entries?.length || data?.scrollResults);
|
||||
if (type === "table") return Boolean(Object.keys(data?.cells || {}).length || Object.keys(data?.rowLabels || {}).length || Object.keys(data?.columnLabels || {}).length);
|
||||
if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
|
||||
if (type === "calendar") return Boolean(data?.events?.length);
|
||||
if (type === "taskPlanner") return Boolean(data?.tasks?.length);
|
||||
if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
|
||||
if (type === "images") return Boolean(data?.images?.length);
|
||||
|
|
@ -67,6 +76,7 @@ export function exportModuleText(type, data) {
|
|||
if (type === "calculator") return exportCalculatorText(data);
|
||||
if (type === "table") return exportTableTsv(data);
|
||||
if (type === "timer") return exportTimerText(data);
|
||||
if (type === "calendar") return exportCalendarText(data);
|
||||
if (type === "taskPlanner") return exportTaskPlannerText(data);
|
||||
if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
|
||||
if (type === "images") return exportImagesText(data);
|
||||
|
|
@ -87,6 +97,7 @@ export function importModuleText(type, text, context) {
|
|||
else if (type === "calculator") data = importCalculatorText(value, context);
|
||||
else if (type === "table") data = importTableTsv(value);
|
||||
else if (type === "timer") data = importTimerText(value, context);
|
||||
else if (type === "calendar") data = importCalendarText(value, context);
|
||||
else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
|
||||
else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
|
||||
else if (type === "images") data = importImagesText(value, context);
|
||||
|
|
@ -644,6 +655,112 @@ function importTimerText(text, context) {
|
|||
return context.normalizeTimerData(data);
|
||||
}
|
||||
|
||||
function parseCalendarDayIndex(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
const weekdayIndex = CALENDAR_WEEKDAYS.indexOf(normalized);
|
||||
if (weekdayIndex >= 0) return weekdayIndex;
|
||||
const parsed = parseInteger(normalized, -1);
|
||||
return parsed >= 0 && parsed <= 6 ? parsed : -1;
|
||||
}
|
||||
|
||||
function parseCalendarAlertOffsets(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => parseInteger(item.trim(), Number.NaN))
|
||||
.filter((offset, index, offsets) => CALENDAR_ALERT_OFFSET_SET.has(offset) && offsets.indexOf(offset) === index)
|
||||
.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
function formatCalendarEventMeta(event, viewMode) {
|
||||
const alertMode = CALENDAR_ALERT_MODE_SET.has(event.alertMode) ? event.alertMode : "off";
|
||||
const meta = {
|
||||
title: event.title,
|
||||
color: event.color && event.color !== "violet" ? event.color : "",
|
||||
repeatable: event.repeatable === false ? "false" : "",
|
||||
alertMode: alertMode !== "off" ? alertMode : "",
|
||||
alerts: alertMode !== "off" ? (event.alertOffsetMinutes || [10]).join(",") : ""
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(event.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
if (monthlyRepeatBy === "weekday") {
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "weekday",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "monthDay",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
day: CALENDAR_WEEKDAYS[event.dayIndex] || "",
|
||||
start: minutesToTimeValue(event.startMinutes),
|
||||
end: event.endMinutes ? minutesToTimeValue(event.endMinutes) : ""
|
||||
};
|
||||
}
|
||||
|
||||
function exportCalendarText(data) {
|
||||
const viewMode = data?.viewMode === "month" ? "month" : "week";
|
||||
const lines = [`@viewMode: ${viewMode}`];
|
||||
if (data?.weekStartsOn === 0) lines.push("@weekStartsOn: 0");
|
||||
(data?.events || []).forEach((event) => {
|
||||
lines.push(`event | ${formatMeta(formatCalendarEventMeta(event, viewMode))}`);
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function importCalendarText(text, context) {
|
||||
const trimmed = String(text || "").trim();
|
||||
if (trimmed.startsWith("{")) {
|
||||
return context.normalizeCalendarData(JSON.parse(trimmed));
|
||||
}
|
||||
|
||||
const data = { initialized: true, viewMode: "week", weekStartsOn: 1, events: [] };
|
||||
splitLines(text).forEach((line, index) => {
|
||||
const cleanLine = line.trim();
|
||||
if (!cleanLine) return;
|
||||
const meta = parseMetaLine(cleanLine);
|
||||
if (meta) {
|
||||
if (meta.key === "viewMode") data.viewMode = meta.value === "month" ? "month" : "week";
|
||||
if (meta.key === "weekStartsOn") data.weekStartsOn = parseInteger(meta.value, 1) === 0 ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||
if (title !== "event") throw lineError(index, `ligne calendrier invalide: ${cleanLine}`);
|
||||
const event = {
|
||||
id: context.uid("calendar"),
|
||||
title: lineMeta.title || "",
|
||||
repeatable: lineMeta.repeatable !== "false",
|
||||
alertMode: CALENDAR_ALERT_MODE_SET.has(lineMeta.alertMode) ? lineMeta.alertMode : "off",
|
||||
alertOffsetMinutes: parseCalendarAlertOffsets(lineMeta.alerts),
|
||||
color: CALENDAR_COLOR_SET.has(lineMeta.color) ? lineMeta.color : "violet"
|
||||
};
|
||||
if (data.viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(lineMeta.repeatBy) ? lineMeta.repeatBy : "monthDay";
|
||||
event.startMinutes = 0;
|
||||
event.monthlyRepeatBy = monthlyRepeatBy;
|
||||
event.repeat = CALENDAR_MONTH_REPEAT_MODE_SET.has(lineMeta.repeat) ? lineMeta.repeat : "none";
|
||||
event.startDate = lineMeta.startDate || "";
|
||||
event.endDate = lineMeta.endDate || event.startDate;
|
||||
} else {
|
||||
event.dayIndex = parseCalendarDayIndex(lineMeta.day);
|
||||
event.startMinutes = timeValueToMinutes(lineMeta.start);
|
||||
if (lineMeta.end) event.endMinutes = timeValueToMinutes(lineMeta.end);
|
||||
}
|
||||
data.events.push(event);
|
||||
});
|
||||
return context.normalizeCalendarData(data);
|
||||
}
|
||||
|
||||
function exportImagesText(data) {
|
||||
return (data.images || []).flatMap((image) => [
|
||||
image.label ? `# ${image.label}` : "#",
|
||||
|
|
|
|||
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Rôle : normalise et compacte les événements du calendrier hebdomadaire et mensuel.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, CALENDAR_VIEW_MODES, formatLocalDateKey, getLocalDayIndex, parseLocalDateKey, WEEK_DAY_COUNT } from "../../modules/calendarUtils.js";
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
const VIEW_MODES = new Set(CALENDAR_VIEW_MODES);
|
||||
const ALERT_MODES = new Set(CALENDAR_ALERT_MODES);
|
||||
const ALERT_OFFSETS = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const COLORS = new Set(CALENDAR_COLORS);
|
||||
const MONTHLY_REPEAT_BY = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const MONTH_REPEAT_MODES = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
|
||||
function normalizeDayIndex(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < WEEK_DAY_COUNT ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeMinuteOfDay(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < 24 * 60 ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeDateKey(value) {
|
||||
const date = parseLocalDateKey(value);
|
||||
return date ? formatLocalDateKey(date) : "";
|
||||
}
|
||||
|
||||
function normalizeExcludedOccurrences(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.map(normalizeDateKey).filter(Boolean))].sort();
|
||||
}
|
||||
|
||||
function normalizeAlertOffsets(value) {
|
||||
const rawOffsets = Array.isArray(value) ? value : [value];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => ALERT_OFFSETS.has(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
export function normalizeCalendarData(data) {
|
||||
const viewMode = VIEW_MODES.has(data?.viewMode) ? data.viewMode : "week";
|
||||
const events = (Array.isArray(data?.events) ? data.events : [])
|
||||
.map((event) => {
|
||||
const title = String(event?.title || "").trim();
|
||||
const dayIndex = normalizeDayIndex(event?.dayIndex);
|
||||
const startDate = normalizeDateKey(event?.startDate);
|
||||
const endDate = normalizeDateKey(event?.endDate || event?.startDate);
|
||||
const parsedStartMinutes = normalizeMinuteOfDay(event?.startMinutes);
|
||||
const startMinutes = viewMode === "month" && parsedStartMinutes < 0 ? 0 : parsedStartMinutes;
|
||||
const monthlyRepeatBy = MONTHLY_REPEAT_BY.has(event?.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
const monthRepeat = MONTH_REPEAT_MODES.has(event?.repeat) ? event.repeat : event?.repeatable === false ? "none" : "monthly";
|
||||
const hasValidDateTarget = viewMode === "month"
|
||||
? startDate && endDate && endDate >= startDate
|
||||
: dayIndex >= 0;
|
||||
if (!title || !hasValidDateTarget || startMinutes < 0) return null;
|
||||
|
||||
const endMinutes = normalizeMinuteOfDay(event?.endMinutes);
|
||||
const alertMode = ALERT_MODES.has(event?.alertMode) ? event.alertMode : "off";
|
||||
const alertOffsetMinutes = normalizeAlertOffsets(event?.alertOffsetMinutes);
|
||||
const color = COLORS.has(event?.color) ? event.color : "violet";
|
||||
const repeatable = viewMode === "month" ? monthRepeat !== "none" : event?.repeatable !== false;
|
||||
const normalized = {
|
||||
id: event?.id || uid("calendar"),
|
||||
title,
|
||||
startMinutes,
|
||||
repeat: viewMode === "month" ? monthlyRepeatBy === "monthDay" && monthRepeat === "weekly" ? "none" : monthRepeat : repeatable ? "weekly" : "none",
|
||||
repeatable,
|
||||
alertMode,
|
||||
alertOffsetMinutes,
|
||||
color
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
normalized.monthlyRepeatBy = monthlyRepeatBy;
|
||||
normalized.startDate = startDate;
|
||||
normalized.endDate = endDate;
|
||||
normalized.dayIndex = dayIndex >= 0 ? dayIndex : getLocalDayIndex(parseLocalDateKey(startDate));
|
||||
normalized.excludedOccurrences = normalizeExcludedOccurrences(event?.excludedOccurrences);
|
||||
} else {
|
||||
normalized.dayIndex = dayIndex;
|
||||
}
|
||||
if (endMinutes > startMinutes) normalized.endMinutes = endMinutes;
|
||||
return normalized;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
initialized: data?.initialized === true || events.length > 0,
|
||||
viewMode,
|
||||
weekStartsOn: Number(data?.weekStartsOn) === 0 ? 0 : 1,
|
||||
events
|
||||
};
|
||||
}
|
||||
|
||||
export function compactCalendarDataForStorage(value) {
|
||||
const normalized = normalizeCalendarData(value);
|
||||
const compact = {};
|
||||
if (normalized.initialized) compact.initialized = true;
|
||||
if (normalized.viewMode !== "week") compact.viewMode = normalized.viewMode;
|
||||
if (normalized.weekStartsOn !== 1) compact.weekStartsOn = normalized.weekStartsOn;
|
||||
if (normalized.events.length) {
|
||||
compact.events = normalized.events.map((event) => {
|
||||
const compactEvent = {
|
||||
id: event.id,
|
||||
title: event.title
|
||||
};
|
||||
if (normalized.viewMode !== "month" || event.startMinutes !== 0) compactEvent.startMinutes = event.startMinutes;
|
||||
if (normalized.viewMode === "month") {
|
||||
compactEvent.startDate = event.startDate;
|
||||
if (event.endDate !== event.startDate) compactEvent.endDate = event.endDate;
|
||||
if (event.repeat !== "monthly") compactEvent.repeat = event.repeat;
|
||||
if (event.monthlyRepeatBy !== "monthDay") compactEvent.monthlyRepeatBy = event.monthlyRepeatBy;
|
||||
if (event.excludedOccurrences.length) compactEvent.excludedOccurrences = event.excludedOccurrences;
|
||||
} else {
|
||||
compactEvent.dayIndex = event.dayIndex;
|
||||
}
|
||||
if (normalized.viewMode !== "month" && event.endMinutes) compactEvent.endMinutes = event.endMinutes;
|
||||
if (normalized.viewMode !== "month" && event.repeatable === false) compactEvent.repeatable = false;
|
||||
if (event.alertMode !== "off") compactEvent.alertMode = event.alertMode;
|
||||
if (event.alertMode !== "off" && (event.alertOffsetMinutes.length !== 1 || event.alertOffsetMinutes[0] !== 10)) compactEvent.alertOffsetMinutes = event.alertOffsetMinutes;
|
||||
if (event.color !== "violet") compactEvent.color = event.color;
|
||||
return compactEvent;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// Rôle : expose le registre strict des compacteurs par type d'outil toolbox.
|
||||
import { compactCalculatorDataForStorage } from "./calculator.js";
|
||||
import { compactCalendarDataForStorage } from "./calendar.js";
|
||||
import { compactChecklistDataForStorage } from "./checklist.js";
|
||||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
|
|
@ -20,6 +21,7 @@ export const MODULE_DATA_COMPACTORS = {
|
|||
counters: compactCountersDataForStorage,
|
||||
combos: compactCombosDataForStorage,
|
||||
calculator: compactCalculatorDataForStorage,
|
||||
calendar: compactCalendarDataForStorage,
|
||||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ const NOTEPAD_RGB_COLOR_MAP = {
|
|||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
return decodeBasicEntitiesDeep(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function decodeBasicEntitiesDeep(value) {
|
||||
let current = String(value || "");
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
const next = decodeBasicEntities(current);
|
||||
if (next === current) return next;
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function decodeBasicEntities(value) {
|
||||
return String(value || "")
|
||||
.replace(/ /gi, " ")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const ID_PREFIXES = {
|
|||
counter: "c",
|
||||
combo: "o",
|
||||
calc: "r",
|
||||
calendar: "h",
|
||||
marker: "k",
|
||||
timer: "z",
|
||||
task: "a",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MODULE_DATA_COMPACTORS } from "./modules/index.js";
|
|||
import { ID_PREFIXES, uid } from "./modules/shared.js";
|
||||
|
||||
export { normalizeCalculatorData } from "./modules/calculator.js";
|
||||
export { normalizeCalendarData } from "./modules/calendar.js";
|
||||
export { clampQty, normalizeChecklistData } from "./modules/checklist.js";
|
||||
export { normalizeCombosData } from "./modules/combos.js";
|
||||
export { normalizeCountersData } from "./modules/counters.js";
|
||||
|
|
@ -250,6 +251,12 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "calendar") {
|
||||
const remapped = { ...compact };
|
||||
if (compact.events) remapped.events = compact.events.map((event) => ({ ...event, id: nextId("calendar") }));
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "taskPlanner") {
|
||||
const taskIdMap = new Map();
|
||||
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
editable: true,
|
||||
libraryCategory: "timeRoutines"
|
||||
},
|
||||
calendar: {
|
||||
label: "Calendrier",
|
||||
defaultTitle: "Calendrier",
|
||||
mode: "Agenda hebdomadaire",
|
||||
icon: "calendar",
|
||||
editable: true,
|
||||
scrollable: true,
|
||||
libraryCategory: "timeRoutines"
|
||||
},
|
||||
taskPlanner: {
|
||||
label: "Planificateur de tâches",
|
||||
defaultTitle: "Planificateur de tâches",
|
||||
|
|
@ -115,7 +124,7 @@ export const LIBRARY_CATEGORY_DEFINITIONS = [
|
|||
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
|
||||
{ key: "references", types: ["images", "imageAnnotation", "links"] },
|
||||
{ key: "calculationData", types: ["calculator", "table"] },
|
||||
{ key: "timeRoutines", types: ["timer", "taskPlanner"] },
|
||||
{ key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] },
|
||||
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
|
||||
];
|
||||
|
||||
|
|
|
|||
99
website/src/hooks/useToolboxAlerts.js
Normal file
99
website/src/hooks/useToolboxAlerts.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Rôle : surveille globalement les alertes internes des outils timer et calendrier.
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getCalendarAlertOffsets, getCurrentCalendarOccurrence, getNextCalendarAlertTime } from "../features/toolboxes/modules/calendarUtils.js";
|
||||
import { getCountdownTargetMs } from "../features/toolboxes/modules/timerUtils.js";
|
||||
import { moduleStorageKey } from "../features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
function getVisibleToolboxIds(route, drawerGameId, links) {
|
||||
const visibleIds = new Set();
|
||||
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
||||
if (toolboxMatch?.[1]) visibleIds.add(toolboxMatch[1]);
|
||||
const drawerToolboxId = drawerGameId ? links[drawerGameId] : "";
|
||||
if (drawerToolboxId) visibleIds.add(drawerToolboxId);
|
||||
return visibleIds;
|
||||
}
|
||||
|
||||
function isAlertAllowed(alertMode, toolboxId, visibleToolboxIds) {
|
||||
if (alertMode === "off") return false;
|
||||
if (alertMode === "visible") return visibleToolboxIds.has(toolboxId);
|
||||
return alertMode === "site";
|
||||
}
|
||||
|
||||
function formatAlertMessage(template, toolbox, label) {
|
||||
return template
|
||||
.replaceAll("{label}", label)
|
||||
.replaceAll("{toolbox}", toolbox.name);
|
||||
}
|
||||
|
||||
export function useToolboxAlerts({ toolboxes, moduleData, links, route, drawerGameId, normalizeTimerData, normalizeCalendarData, textContent, notify }) {
|
||||
const previousTargetsRef = useRef(new Map());
|
||||
const resumedCalendarEventsRef = useRef(new Set());
|
||||
const lastTickRef = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const visibleToolboxIds = getVisibleToolboxIds(route, drawerGameId, links);
|
||||
const timerAlertTemplate = textContent?.timer?.alertMessage || "Compte à rebours terminé : {label}";
|
||||
const calendarAlertTemplate = textContent?.calendar?.alertMessage || "Événement à venir : {label}";
|
||||
const calendarResumeTemplate = textContent?.calendar?.resumeAlertMessage || "Événement en cours : {label}";
|
||||
|
||||
function tick() {
|
||||
const nowMs = Date.now();
|
||||
const lastTickMs = lastTickRef.current;
|
||||
const nextTargets = new Map();
|
||||
|
||||
toolboxes.forEach((toolbox) => {
|
||||
toolbox.modules.forEach((module) => {
|
||||
if (module.type === "timer") {
|
||||
const data = normalizeTimerData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
||||
|
||||
data.countdowns.forEach((countdown) => {
|
||||
if (!isAlertAllowed(countdown.alertMode, toolbox.id, visibleToolboxIds)) return;
|
||||
|
||||
const key = `${toolbox.id}:${module.id}:${countdown.id}`;
|
||||
const targetMs = getCountdownTargetMs(countdown, nowMs);
|
||||
const previousTargetMs = previousTargetsRef.current.get(key) || targetMs;
|
||||
nextTargets.set(key, targetMs);
|
||||
|
||||
if (!targetMs || previousTargetMs <= lastTickMs || previousTargetMs > nowMs) return;
|
||||
notify(formatAlertMessage(timerAlertTemplate, toolbox, countdown.label));
|
||||
});
|
||||
}
|
||||
|
||||
if (module.type === "calendar") {
|
||||
const data = normalizeCalendarData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
||||
|
||||
data.events.forEach((event) => {
|
||||
if (!isAlertAllowed(event.alertMode, toolbox.id, visibleToolboxIds)) return;
|
||||
|
||||
const key = `${toolbox.id}:${module.id}:${event.id}`;
|
||||
getCalendarAlertOffsets(event).forEach((offset) => {
|
||||
const alertKey = `${key}:alert:${offset}`;
|
||||
const alertMs = getNextCalendarAlertTime({ ...event, alertOffsetMinutes: offset }, nowMs);
|
||||
const previousAlertMs = previousTargetsRef.current.get(alertKey) || alertMs;
|
||||
if (alertMs) nextTargets.set(alertKey, alertMs);
|
||||
|
||||
if (alertMs && previousAlertMs > lastTickMs && previousAlertMs <= nowMs) {
|
||||
notify(formatAlertMessage(calendarAlertTemplate, toolbox, event.title));
|
||||
}
|
||||
});
|
||||
|
||||
const currentOccurrence = getCurrentCalendarOccurrence(event, nowMs);
|
||||
if (!currentOccurrence) return;
|
||||
const resumeKey = `${key}:${currentOccurrence.startMs}`;
|
||||
if (resumedCalendarEventsRef.current.has(resumeKey)) return;
|
||||
resumedCalendarEventsRef.current.add(resumeKey);
|
||||
notify(formatAlertMessage(calendarResumeTemplate, toolbox, event.title));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
previousTargetsRef.current = nextTargets;
|
||||
lastTickRef.current = nowMs;
|
||||
}
|
||||
|
||||
tick();
|
||||
const intervalId = window.setInterval(tick, 500);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [drawerGameId, links, moduleData, normalizeCalendarData, normalizeTimerData, notify, route, textContent, toolboxes]);
|
||||
}
|
||||
|
|
@ -9,12 +9,20 @@ import { useIndexedToolboxes } from "./features/toolboxes/storage/useIndexedTool
|
|||
import { useToolboxActions } from "./features/toolboxes/useToolboxActions.js";
|
||||
import { useAppData } from "./hooks/useAppData.js";
|
||||
import { useGameFilters } from "./hooks/useGameFilters.js";
|
||||
import { useTimerAlerts } from "./hooks/useTimerAlerts.js";
|
||||
import { useToolboxAlerts } from "./hooks/useToolboxAlerts.js";
|
||||
import { RouteContent } from "./router/RouteContent.jsx";
|
||||
import { useHashRoute } from "./router/hashRouter.js";
|
||||
import { normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||
import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer })));
|
||||
const DEFAULT_NOTIFICATION_DURATION_MS = 120000;
|
||||
const SHORT_NOTIFICATION_DURATION_MS = 5000;
|
||||
|
||||
function getNotificationDuration(message, options = {}) {
|
||||
if (Number.isFinite(options.durationMs)) return Math.max(1000, Number(options.durationMs));
|
||||
const normalized = String(message || "").toLowerCase();
|
||||
return /\bcopi[ée]|\bcoll[ée]|presse-papiers/.test(normalized) ? SHORT_NOTIFICATION_DURATION_MS : DEFAULT_NOTIFICATION_DURATION_MS;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const route = useHashRoute();
|
||||
|
|
@ -31,9 +39,9 @@ function App() {
|
|||
const handleStorageError = useCallback((message) => setStorageError(message), []);
|
||||
const store = useIndexedToolboxes(handleStorageError);
|
||||
|
||||
const notify = useCallback((message) => {
|
||||
const notify = useCallback((message, options = {}) => {
|
||||
setNotifications((current) => [
|
||||
{ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, message },
|
||||
{ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, message, durationMs: getNotificationDuration(message, options) },
|
||||
...current
|
||||
].slice(0, 5));
|
||||
}, []);
|
||||
|
|
@ -44,7 +52,7 @@ function App() {
|
|||
|
||||
useEffect(() => {
|
||||
function handleNotification(event) {
|
||||
if (event.detail?.message) notify(event.detail.message);
|
||||
if (event.detail?.message) notify(event.detail.message, event.detail);
|
||||
}
|
||||
|
||||
window.addEventListener("sokkog:notify", handleNotification);
|
||||
|
|
@ -70,14 +78,15 @@ function App() {
|
|||
setImage
|
||||
});
|
||||
|
||||
useTimerAlerts({
|
||||
useToolboxAlerts({
|
||||
toolboxes: store.toolboxes,
|
||||
moduleData: store.moduleData,
|
||||
links: store.links,
|
||||
route,
|
||||
drawerGameId,
|
||||
normalizeTimerData,
|
||||
textContent: siteContent?.toolboxes?.modules?.timer,
|
||||
normalizeCalendarData,
|
||||
textContent: siteContent?.toolboxes?.modules,
|
||||
notify
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
|
|
@ -31,6 +32,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: lazy(() => import("../features/toolboxes/modules/CountersModule.jsx").then((module) => ({ default: module.CountersModule }))),
|
||||
combos: lazy(() => import("../features/toolboxes/modules/CombosModule.jsx").then((module) => ({ default: module.CombosModule }))),
|
||||
calculator: lazy(() => import("../features/toolboxes/modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule }))),
|
||||
calendar: lazy(() => import("../features/toolboxes/modules/CalendarModule.jsx").then((module) => ({ default: module.CalendarModule }))),
|
||||
table: lazy(() => import("../features/toolboxes/modules/TableModule.jsx").then((module) => ({ default: module.TableModule }))),
|
||||
timer: lazy(() => import("../features/toolboxes/modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule }))),
|
||||
taskPlanner: lazy(() => import("../features/toolboxes/modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule }))),
|
||||
|
|
@ -94,12 +96,15 @@ function getToolDescription(siteContent, definition) {
|
|||
}
|
||||
|
||||
function getLibraryCategories(modules, content) {
|
||||
const modulesByType = new Map((modules || []).map((module) => [module.type, module]));
|
||||
const modulesByType = new Map();
|
||||
(modules || []).forEach((module) => {
|
||||
modulesByType.set(module.type, [...(modulesByType.get(module.type) || []), module]);
|
||||
});
|
||||
return LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({
|
||||
...category,
|
||||
title: content.categories?.[category.key] || category.key,
|
||||
description: content.categoryDescriptions?.[category.key] || "",
|
||||
modules: category.types.map((type) => modulesByType.get(type)).filter(Boolean)
|
||||
modules: category.types.flatMap((type) => modulesByType.get(type) || [])
|
||||
})).filter((category) => category.modules.length);
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +154,7 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
|
|
@ -245,7 +251,11 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||
description={getToolDescription(siteContent, getToolboxModuleDefinition(module.type))}
|
||||
categoryTitle={activeCategory.title}
|
||||
docs={content.toolDocs?.[module.type]}
|
||||
docs={{
|
||||
...(content.toolDocs?.[module.type] || {}),
|
||||
...(module.controls ? { controls: module.controls } : {}),
|
||||
...(module.importFormat ? { importFormat: module.importFormat } : {})
|
||||
}}
|
||||
featureHeading={content.featureHeading}
|
||||
advancedHeading={content.advancedHeading}
|
||||
advancedCopyLabel={content.copyExampleLabel}
|
||||
|
|
|
|||
|
|
@ -613,12 +613,8 @@ span {
|
|||
}
|
||||
|
||||
.library-doc-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
max-height: calc(100vh - var(--space-10));
|
||||
gap: var(--space-4);
|
||||
overflow: auto;
|
||||
padding: var(--space-4);
|
||||
scroll-margin-top: var(--space-5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,31 @@
|
|||
-webkit-mask-image: url("/static/icons/clock.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar {
|
||||
mask-image: url("/static/icons/calendar.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar-month {
|
||||
mask-image: url("/static/icons/calendar-month.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar-month.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar-week {
|
||||
mask-image: url("/static/icons/calendar-week.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar-week.svg");
|
||||
}
|
||||
|
||||
.ui-icon-day-number {
|
||||
mask-image: url("/static/icons/day-number.svg");
|
||||
-webkit-mask-image: url("/static/icons/day-number.svg");
|
||||
}
|
||||
|
||||
.ui-icon-day-name {
|
||||
mask-image: url("/static/icons/day-name.svg");
|
||||
-webkit-mask-image: url("/static/icons/day-name.svg");
|
||||
}
|
||||
|
||||
.ui-icon-stopwatch {
|
||||
mask-image: url("/static/icons/stopwatch.svg");
|
||||
-webkit-mask-image: url("/static/icons/stopwatch.svg");
|
||||
|
|
@ -362,6 +387,11 @@
|
|||
-webkit-mask-image: url("/static/icons/trashcan.svg");
|
||||
}
|
||||
|
||||
.ui-icon-trashcan-one {
|
||||
mask-image: url("/static/icons/trashcan-one.svg");
|
||||
-webkit-mask-image: url("/static/icons/trashcan-one.svg");
|
||||
}
|
||||
|
||||
.game-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
|
|
|
|||
|
|
@ -243,7 +243,6 @@
|
|||
}
|
||||
|
||||
.library-doc-nav {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
1113
website/src/styles/toolboxes/_calendar.scss
Normal file
1113
website/src/styles/toolboxes/_calendar.scss
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@
|
|||
@use "counters";
|
||||
@use "calculator";
|
||||
@use "timer";
|
||||
@use "calendar";
|
||||
@use "calculator-results";
|
||||
@use "table";
|
||||
@use "shared-controls";
|
||||
|
|
|
|||
|
|
@ -240,6 +240,11 @@
|
|||
-webkit-mask-image: url("/static/icons/clock.svg");
|
||||
}
|
||||
|
||||
.module-icon-calendar {
|
||||
mask-image: url("/static/icons/calendar.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar.svg");
|
||||
}
|
||||
|
||||
.module-icon-stopwatch {
|
||||
mask-image: url("/static/icons/stopwatch.svg");
|
||||
-webkit-mask-image: url("/static/icons/stopwatch.svg");
|
||||
|
|
|
|||
|
|
@ -127,7 +127,10 @@
|
|||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-reset-override input,
|
||||
.task-planner-relations select {
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -140,7 +143,10 @@
|
|||
.task-planner-add-form select option,
|
||||
.task-planner-type-select option,
|
||||
.task-planner-reset-override select option,
|
||||
.task-planner-relations select option {
|
||||
.task-planner-relations select option,
|
||||
.combos-device-row select option,
|
||||
.equipment-planner-socket-links select option,
|
||||
.equipment-planner-socket-controls select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,10 @@
|
|||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-relations select {
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
|
|
@ -113,7 +116,13 @@
|
|||
.task-planner-reset-override select:hover,
|
||||
.task-planner-reset-override select:focus,
|
||||
.task-planner-relations select:hover,
|
||||
.task-planner-relations select:focus {
|
||||
.task-planner-relations select:focus,
|
||||
.combos-device-row select:hover,
|
||||
.combos-device-row select:focus,
|
||||
.equipment-planner-socket-links select:hover,
|
||||
.equipment-planner-socket-links select:focus,
|
||||
.equipment-planner-socket-controls select:hover,
|
||||
.equipment-planner-socket-controls select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue