redesign app on top of the kluk.fr charte
- drop Bulma and Font Awesome, plain CSS built on charte.css - dashboard: GitHub-like calendar grouped by year, selected day panel (sticky side column on desktop, bottom bar on mobile), keyboard navigation - public page: today's mood and last months fit in one screen, journal hidden - day log page: mode picker as pills, illustration follows the chosen mode - login, password and invitation screens in French, global flash messages - add User#current_mode_since Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ad251e7558
commit
7a7fa4c095
37 changed files with 869 additions and 419 deletions
|
|
@ -1,62 +0,0 @@
|
||||||
// Raccords entre Bulma et la charte : ce que ni l'un ni l'autre ne règle seul.
|
|
||||||
|
|
||||||
// Les titres Bulma suivent ceux de la charte : Young Serif en 400.
|
|
||||||
.title,
|
|
||||||
.subtitle {
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.005em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// La charte souligne tous les liens ; pas les liens habillés en bouton ou en étiquette.
|
|
||||||
a.button,
|
|
||||||
a.tag,
|
|
||||||
a.navbar-item,
|
|
||||||
a.dropdown-item {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// La charte ne prépare les <button class="bouton"> que dans un .formulaire :
|
|
||||||
// on les prépare partout (button_to, f.submit…).
|
|
||||||
button.bouton,
|
|
||||||
input.bouton {
|
|
||||||
border: 0;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button.bouton:disabled,
|
|
||||||
input.bouton:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// button_to enveloppe son bouton dans un formulaire : le garder en ligne dans la navigation.
|
|
||||||
.nav form.button_to {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// La charte ne vise que les champs email et les zones de texte : on étend aux autres types.
|
|
||||||
.formulaire {
|
|
||||||
input[type="text"],
|
|
||||||
input[type="password"],
|
|
||||||
input[type="date"],
|
|
||||||
input[type="number"],
|
|
||||||
select {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
border: 2px solid var(--bordure-champ);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
background: var(--blanc);
|
|
||||||
color: var(--encre);
|
|
||||||
font: inherit;
|
|
||||||
line-height: 1.4;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 3px solid var(--vert);
|
|
||||||
outline-offset: 1px;
|
|
||||||
border-color: var(--encre);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
485
app/assets/stylesheets/app.css
Normal file
485
app/assets/stylesheets/app.css
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
/*
|
||||||
|
* Styles propres à l'application, construits sur la charte (charte.css).
|
||||||
|
* La couleur d'un mode est choisie par l'utilisateur : elle arrive dans la variable
|
||||||
|
* --humeur, posée en style en ligne. Le reste de l'interface reste neutre
|
||||||
|
* (porcelaine, encre, chêne) pour que les humeurs soient les seules couleurs.
|
||||||
|
*
|
||||||
|
* Sommaire
|
||||||
|
* 1. Compléments à la charte (boutons, champs, navigation)
|
||||||
|
* 2. Messages flash
|
||||||
|
* 3. Carte d'humeur (illustration posée sur la Kluk)
|
||||||
|
* 4. Légende
|
||||||
|
* 5. Calendrier des humeurs
|
||||||
|
* 6. Tableau de bord
|
||||||
|
* 7. Page publique
|
||||||
|
* 8. Page d'un jour (journal)
|
||||||
|
* 9. Impression
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---------- 1. Compléments à la charte ---------- */
|
||||||
|
|
||||||
|
/* La charte ne prépare les <button class="bouton"> que dans un .formulaire */
|
||||||
|
button.bouton,
|
||||||
|
input.bouton {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button_to enveloppe son bouton dans un formulaire : le garder en ligne */
|
||||||
|
.nav form.button_to { display: inline; }
|
||||||
|
|
||||||
|
.formulaire input[type="password"],
|
||||||
|
.formulaire input[type="text"] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
border: 2px solid var(--bordure-champ);
|
||||||
|
border-radius: var(--arrondi-petit);
|
||||||
|
background: var(--blanc);
|
||||||
|
color: var(--encre);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.formulaire input[type="password"]:focus-visible,
|
||||||
|
.formulaire input[type="text"]:focus-visible {
|
||||||
|
outline: 3px solid var(--vert);
|
||||||
|
outline-offset: 1px;
|
||||||
|
border-color: var(--encre);
|
||||||
|
}
|
||||||
|
.formulaire .formulaire-erreur { margin: 0 0 1.75rem; }
|
||||||
|
|
||||||
|
.texte-etroit { max-width: calc(30rem + 2 * var(--gouttiere)); }
|
||||||
|
.texte-etroit h1 { font-size: clamp(2.1rem, 1.7rem + 2vw, 2.75rem); }
|
||||||
|
.texte-etroit .formulaire { margin-top: 1.5rem; }
|
||||||
|
|
||||||
|
/* ---------- 2. Messages flash ---------- */
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5rem var(--gouttiere) 0;
|
||||||
|
}
|
||||||
|
.flash-message {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-left: 4px solid var(--vert);
|
||||||
|
background: var(--surface);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.flash-alerte {
|
||||||
|
border-left-color: var(--rouge);
|
||||||
|
background: var(--erreur-fond);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 3. Carte d'humeur ---------- */
|
||||||
|
|
||||||
|
/* L'illustration du mode, dessinée à l'encre sur papier blanc, posée sur le socle
|
||||||
|
de la Kluk : une pilule à la couleur de l'humeur, avec son voyant. */
|
||||||
|
.carte-humeur {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.carte-humeur img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: contain;
|
||||||
|
background: var(--blanc);
|
||||||
|
border: 2.5px solid var(--encre);
|
||||||
|
border-radius: var(--arrondi);
|
||||||
|
}
|
||||||
|
.carte-humeur::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 1rem;
|
||||||
|
margin: 0.4rem 6% 0;
|
||||||
|
border: 2.5px solid var(--encre);
|
||||||
|
border-radius: var(--arrondi-pilule);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle, var(--porcelaine) 0 2.5px, transparent 3px),
|
||||||
|
var(--humeur, var(--porcelaine-fonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 4. Légende ---------- */
|
||||||
|
|
||||||
|
.pastille {
|
||||||
|
flex: none;
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.85rem;
|
||||||
|
height: 0.85rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--humeur);
|
||||||
|
}
|
||||||
|
.pastille-supposee {
|
||||||
|
background: color-mix(in srgb, var(--encre-douce) 35%, var(--porcelaine));
|
||||||
|
}
|
||||||
|
.pastille-inconnue {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1.5px dashed var(--bordure-champ);
|
||||||
|
}
|
||||||
|
.pastille-journal {
|
||||||
|
position: relative;
|
||||||
|
background: var(--porcelaine-fonce);
|
||||||
|
}
|
||||||
|
.pastille-journal::after,
|
||||||
|
.jour-journal::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--blanc);
|
||||||
|
box-shadow: 0 0 0 1px var(--encre);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legende {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.legende ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem 1.4rem;
|
||||||
|
}
|
||||||
|
.legende li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.legende .legende-etats {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
color: var(--encre-douce);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 5. Calendrier des humeurs ---------- */
|
||||||
|
|
||||||
|
/* Une colonne par semaine, une ligne par jour (du lundi au dimanche), comme sur GitHub. */
|
||||||
|
.calendrier-humeurs {
|
||||||
|
--case: 0.95rem;
|
||||||
|
--ecart: 3px;
|
||||||
|
}
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
.calendrier-humeurs { --case: 0.9rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.annee-titre {
|
||||||
|
margin: 2.25rem 0 0.75rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annee-mois {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mois-nom {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: var(--encre-douce);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mois-jours,
|
||||||
|
.semaine-jours {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(7, var(--case));
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: var(--case);
|
||||||
|
gap: var(--ecart);
|
||||||
|
}
|
||||||
|
|
||||||
|
.semaine-jours {
|
||||||
|
grid-auto-columns: auto;
|
||||||
|
padding-top: 1.7rem;
|
||||||
|
margin-right: 0.15rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: var(--case);
|
||||||
|
color: var(--encre-douce);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: var(--case);
|
||||||
|
height: var(--case);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.jour-filled {
|
||||||
|
background: var(--humeur);
|
||||||
|
}
|
||||||
|
/* Mode supposé : la couleur de la veille, pâlie */
|
||||||
|
.jour-guessed {
|
||||||
|
background: color-mix(in srgb, var(--humeur) 35%, var(--porcelaine));
|
||||||
|
}
|
||||||
|
.jour-unknown {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1.5px dashed var(--bordure-champ);
|
||||||
|
}
|
||||||
|
.jour-empty {
|
||||||
|
background: var(--porcelaine-fonce);
|
||||||
|
}
|
||||||
|
.jour-future {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.jour { cursor: pointer; }
|
||||||
|
button.jour[aria-pressed="true"] {
|
||||||
|
outline: 2.5px solid var(--encre);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
button.jour:focus-visible {
|
||||||
|
outline: 3px solid var(--encre);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
|
||||||
|
button.jour { transition: transform 0.12s ease; }
|
||||||
|
button.jour:hover { transform: scale(1.3); z-index: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 6. Tableau de bord ---------- */
|
||||||
|
|
||||||
|
.tableau {
|
||||||
|
max-width: var(--largeur-entete);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5rem var(--gouttiere) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableau-titre {
|
||||||
|
margin: 0.5rem 0 1.25rem;
|
||||||
|
font-size: clamp(2.1rem, 1.7rem + 2vw, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableau-calendrier { padding-bottom: 3rem; }
|
||||||
|
|
||||||
|
.detail-date {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.detail-date::first-letter { text-transform: uppercase; }
|
||||||
|
|
||||||
|
.detail-mode {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.detail-statut {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--encre-douce);
|
||||||
|
}
|
||||||
|
.detail-statut:not(:empty)::before { content: "("; }
|
||||||
|
.detail-statut:not(:empty)::after { content: ")"; }
|
||||||
|
|
||||||
|
.detail-journal {
|
||||||
|
margin: 0.75rem 0 1.25rem;
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.detail-journal-vide {
|
||||||
|
color: var(--encre-douce);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sur mobile, le jour choisi reste en bas de l'écran pendant qu'on parcourt le calendrier */
|
||||||
|
@media (max-width: 59.99rem) {
|
||||||
|
.detail {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4.75rem minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
margin: 0 calc(-1 * var(--gouttiere));
|
||||||
|
padding: 0.9rem var(--gouttiere) 1rem;
|
||||||
|
background: var(--porcelaine);
|
||||||
|
border-top: 3px solid var(--encre);
|
||||||
|
}
|
||||||
|
.detail .carte-humeur img { border-width: 2px; border-radius: var(--arrondi-petit); }
|
||||||
|
.detail .carte-humeur::after { height: 0.6rem; margin-top: 0.25rem; border-width: 2px; background: var(--humeur, var(--porcelaine-fonce)); }
|
||||||
|
.detail-date { font-size: 1.1rem; }
|
||||||
|
.detail-mode { font-size: 1rem; }
|
||||||
|
.detail-journal {
|
||||||
|
margin: 0.35rem 0 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 60rem) {
|
||||||
|
.tableau {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 19rem;
|
||||||
|
gap: 4rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.detail {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
.detail-texte { margin-top: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 7. Page publique ---------- */
|
||||||
|
|
||||||
|
/* Sur ordinateur, tout tient dans le premier écran : l'illustration à gauche,
|
||||||
|
l'humeur du jour et les derniers mois à droite. */
|
||||||
|
.public {
|
||||||
|
max-width: var(--largeur-entete);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5rem var(--gouttiere) 4rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.75rem;
|
||||||
|
}
|
||||||
|
.carte-humeur-grande {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-qui {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--encre-douce);
|
||||||
|
}
|
||||||
|
.public-mode {
|
||||||
|
margin: 0.2rem 0 0.6rem;
|
||||||
|
font-size: clamp(2.25rem, 1.7rem + 2.4vw, 3.25rem);
|
||||||
|
}
|
||||||
|
.public-depuis {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-historique h2 {
|
||||||
|
margin: 2.5rem 0 0.75rem;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.public-historique .annee-mois { margin-top: 1rem; }
|
||||||
|
|
||||||
|
@media (min-width: 52rem) {
|
||||||
|
.public {
|
||||||
|
grid-template-columns: min(20rem, 48vh) minmax(0, 1fr);
|
||||||
|
gap: 3.5rem;
|
||||||
|
align-items: start;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
.carte-humeur-grande { max-width: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 8. Page d'un jour (journal) ---------- */
|
||||||
|
|
||||||
|
.page-jour {
|
||||||
|
max-width: 56rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5rem var(--gouttiere) 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retour {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-jour-titre {
|
||||||
|
margin: 0.5rem 0 1.5rem;
|
||||||
|
font-size: clamp(2rem, 1.6rem + 2vw, 2.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-jour .carte-humeur {
|
||||||
|
max-width: 11rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-jour .formulaire { margin-top: 1.75rem; }
|
||||||
|
|
||||||
|
.choix-modes {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.choix-modes li { position: relative; }
|
||||||
|
.choix-modes input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.choix-modes label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.5rem 1rem 0.5rem 0.85rem;
|
||||||
|
border: 2px solid var(--bordure-champ);
|
||||||
|
border-radius: var(--arrondi-pilule);
|
||||||
|
background: var(--blanc);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.choix-modes label:hover { border-color: var(--encre); }
|
||||||
|
.choix-modes input:checked + label {
|
||||||
|
border-color: var(--encre);
|
||||||
|
box-shadow: inset 0 0 0 1.5px var(--encre);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.choix-modes input:focus-visible + label {
|
||||||
|
outline: 3px solid var(--encre);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 48rem) {
|
||||||
|
.page-jour {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 17rem minmax(0, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"carte entete"
|
||||||
|
"carte formulaire";
|
||||||
|
column-gap: 3.5rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.page-jour-entete { grid-area: entete; }
|
||||||
|
.page-jour .carte-humeur {
|
||||||
|
grid-area: carte;
|
||||||
|
max-width: none;
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.page-jour .formulaire { grid-area: formulaire; margin-top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 9. Impression ---------- */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.detail { position: static; border: 0; }
|
||||||
|
.page-jour .formulaire .actions { display: none; }
|
||||||
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// Bulma, habillé aux couleurs et typographies de la charte Kluk (charte.css).
|
|
||||||
// Les valeurs reprennent les variables de charte.css : garder les deux en phase.
|
|
||||||
// Version sans mode sombre : la charte n'en a pas, et Bulma basculerait sinon
|
|
||||||
// champs, titres et fonds en sombre quand le système le demande.
|
|
||||||
@use 'bulma/versions/bulma-no-dark-mode' with (
|
|
||||||
$family-primary: ("Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, sans-serif),
|
|
||||||
$family-secondary: ("Young Serif", Georgia, serif),
|
|
||||||
$primary: #2E2A26, // --encre : les boutons de la charte sont à l'encre
|
|
||||||
$link: #3F7A3B, // --vert-lien
|
|
||||||
$success: #5F9B59, // --vert
|
|
||||||
$warning: #EDA33D, // --orange
|
|
||||||
$danger: #D8573F, // --rouge
|
|
||||||
$info: #8D97A8, // --gris
|
|
||||||
$text: #2E2A26, // --encre
|
|
||||||
$text-strong: #2E2A26,
|
|
||||||
$text-weak: #5A534C, // --encre-douce
|
|
||||||
$scheme-main: #F5F6F0, // --porcelaine
|
|
||||||
$background: #E9ECE2, // --porcelaine-fonce
|
|
||||||
$border: #8A8279, // --bordure-champ
|
|
||||||
$radius: 0.5rem, // --arrondi-petit
|
|
||||||
$radius-large: 0.75rem, // --arrondi
|
|
||||||
);
|
|
||||||
|
|
||||||
// La charte vient après Bulma pour que ses règles de base l'emportent.
|
|
||||||
@use 'charte';
|
|
||||||
@use 'charte-bulma';
|
|
||||||
|
|
||||||
// Styles propres à l'application
|
|
||||||
@use 'kluk';
|
|
||||||
4
app/assets/stylesheets/application.scss
Normal file
4
app/assets/stylesheets/application.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Point d'entrée des styles, compilé par cssbundling (yarn build:css).
|
||||||
|
// La charte d'abord (variables, base, composants), puis les styles propres à l'application.
|
||||||
|
@use 'charte';
|
||||||
|
@use 'app';
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Extraite de assets/css/style.scss (site kluk.fr), en CSS pur, sans Sass.
|
* Extraite de assets/css/style.scss (site kluk.fr), en CSS pur, sans Sass.
|
||||||
* Copie de charte/css/kluk.css : seuls les chemins des polices changent, ils
|
* Copie de charte/css/kluk.css : seuls les chemins des polices changent, ils
|
||||||
* pointent vers app/assets/fonts/ (Propshaft les réécrit avec l'empreinte).
|
* pointent vers app/assets/fonts/ (Propshaft les réécrit avec l'empreinte).
|
||||||
* Ne pas modifier ici : les ajustements pour Bulma sont dans _charte-bulma.scss.
|
* Ne pas modifier ici : les compléments propres à l’application sont dans app.css.
|
||||||
*
|
*
|
||||||
* Palette : la porcelaine du matin, l'encre du thé, le chêne de la table,
|
* Palette : la porcelaine du matin, l'encre du thé, le chêne de la table,
|
||||||
* et les quatre couleurs des tasses en accent.
|
* et les quatre couleurs des tasses en accent.
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Styles propres à l'application (tableau de bord, journal).
|
|
||||||
* La charte graphique est dans charte.css, les raccords avec Bulma dans _charte-bulma.scss.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.tracker {
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs {
|
|
||||||
height: 60vh;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-day {
|
|
||||||
background-color: var(--porcelaine);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
align-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day {
|
|
||||||
border: 1px;
|
|
||||||
margin: 4px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.has-day-log {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day.has-day-log::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1px;
|
|
||||||
right: 1px;
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-day {
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 0 3px var(--porcelaine), 0 0 0 5px var(--encre);
|
|
||||||
margin: 4px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-day {
|
|
||||||
border: 1px dashed var(--bordure-champ);
|
|
||||||
margin: 4px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
margin: 30px;
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@ class HomeController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@user = User.find_by(username: request.subdomain)
|
@user = User.find_by(username: request.subdomain)
|
||||||
if @user&.public?
|
if @user&.public?
|
||||||
render "moods/index"
|
render "moods/public"
|
||||||
else
|
else
|
||||||
redirect_to dashboard_path
|
redirect_to dashboard_path
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ class InvitationsController < ApplicationController
|
||||||
password_params = params.expect(user: [ :password, :password_confirmation ])
|
password_params = params.expect(user: [ :password, :password_confirmation ])
|
||||||
|
|
||||||
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
|
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
|
||||||
redirect_to root_path, notice: "Account activated!"
|
redirect_to root_path, notice: "Compte activé. Bienvenue !"
|
||||||
else
|
else
|
||||||
redirect_to invitation_path(params[:token]), alert: "Passwords did not match."
|
redirect_to invitation_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -20,6 +20,6 @@ class InvitationsController < ApplicationController
|
||||||
def set_user_by_token
|
def set_user_by_token
|
||||||
@user = User.find_by_invitation_token(params[:token])
|
@user = User.find_by_invitation_token(params[:token])
|
||||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||||
redirect_to root_path, alert: "Invitation link is invalid or has expired."
|
redirect_to root_path, alert: "Ce lien d’invitation n’est plus valable."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class PasswordsController < ApplicationController
|
||||||
PasswordsMailer.reset(user).deliver_later
|
PasswordsMailer.reset(user).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
|
redirect_to new_session_path, notice: "Si un compte existe avec cette adresse, un lien pour changer de mot de passe vient de lui être envoyé."
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
|
@ -18,9 +18,9 @@ class PasswordsController < ApplicationController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @user.update(params.permit(:password, :password_confirmation))
|
if @user.update(params.permit(:password, :password_confirmation))
|
||||||
redirect_to new_session_path, notice: "Password has been reset."
|
redirect_to new_session_path, notice: "Mot de passe changé. Vous pouvez vous connecter."
|
||||||
else
|
else
|
||||||
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
redirect_to edit_password_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -28,6 +28,6 @@ class PasswordsController < ApplicationController
|
||||||
def set_user_by_token
|
def set_user_by_token
|
||||||
@user = User.find_by_password_reset_token!(params[:token])
|
@user = User.find_by_password_reset_token!(params[:token])
|
||||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||||
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
|
redirect_to new_password_path, alert: "Ce lien n’est plus valable. Demandez-en un nouveau."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
allow_unauthenticated_access only: %i[ new create ]
|
allow_unauthenticated_access only: %i[ new create ]
|
||||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
|
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Trop de tentatives. Réessayez dans quelques minutes." }
|
||||||
|
|
||||||
def new
|
def new
|
||||||
end
|
end
|
||||||
|
|
@ -10,7 +10,7 @@ class SessionsController < ApplicationController
|
||||||
start_new_session_for user
|
start_new_session_for user
|
||||||
redirect_to after_authentication_url
|
redirect_to after_authentication_url
|
||||||
else
|
else
|
||||||
redirect_to new_session_path, alert: "Try another email address or password."
|
redirect_to new_session_path, alert: "Adresse mail ou mot de passe incorrect."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
module MoodsHelper
|
module MoodsHelper
|
||||||
|
# État d'un jour du calendrier :
|
||||||
|
# :filled une tasse a été posée ce jour-là
|
||||||
|
# :guessed pas de tasse posée, on suppose que le mode de la veille continue
|
||||||
|
# :unknown pas de tasse posée, et l'utilisateur ne veut pas de supposition
|
||||||
|
# :empty avant le premier enregistrement
|
||||||
|
# :future après aujourd'hui
|
||||||
def day_status(mood, user)
|
def day_status(mood, user)
|
||||||
|
return :future if mood[:recorded_at].to_date > Date.current
|
||||||
return :filled if mood[:mode].present?
|
return :filled if mood[:mode].present?
|
||||||
return :guessed if mood[:guess].present? && user.guess?
|
return :guessed if mood[:guess].present? && user.guess?
|
||||||
return :unknown if mood[:guess].present? && !user.guess?
|
return :unknown if mood[:guess].present? && !user.guess?
|
||||||
|
|
@ -10,30 +17,64 @@ module MoodsHelper
|
||||||
case day_status(mood, user)
|
case day_status(mood, user)
|
||||||
when :filled then mood[:mode]
|
when :filled then mood[:mode]
|
||||||
when :guessed then mood[:guess]
|
when :guessed then mood[:guess]
|
||||||
when :unknown, :empty then Mode.new(label: "unknown", color: "white")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
=begin
|
|
||||||
def mode_for(mood, user)
|
|
||||||
if user.guess?
|
|
||||||
mood[:mode] || mood[:guess] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
|
|
||||||
else
|
|
||||||
mood[:mode] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
=end
|
|
||||||
|
|
||||||
def style_for_mode(mode, status)
|
# Les années du calendrier, la plus récente d'abord ; les mois dans l'ordre à l'intérieur.
|
||||||
|
def history_by_year(history)
|
||||||
|
history.group_by { |month| month[:month].year }.sort_by { |year, _| -year }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_day(history, date)
|
||||||
|
history.each do |month|
|
||||||
|
month[:weeks].each do |week|
|
||||||
|
week.each { |mood| return mood if mood[:recorded_at].to_date == date }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def date_longue(date)
|
||||||
|
l(date, format: "%A %-d %B %Y")
|
||||||
|
end
|
||||||
|
|
||||||
|
def libelle_statut(status)
|
||||||
case status
|
case status
|
||||||
when :empty then ""
|
when :guessed then "supposé"
|
||||||
when :unknown then "background-color: #{mode.color}; border: 2px double grey;"
|
when :unknown then "pas de tasse posée"
|
||||||
else "background-color: #{mode.color};"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def css_class_for_day(status, day_log)
|
# Un jour du calendrier : un bouton pour le propriétaire, une simple case pour les visiteurs.
|
||||||
css_class = status == :empty ? "day empty-day" : "day"
|
def case_jour(mood, user, interactive:, selected: false)
|
||||||
css_class += " has-day-log" if day_log
|
date = mood[:recorded_at].to_date
|
||||||
css_class
|
status = day_status(mood, user)
|
||||||
|
mode = mode_for(mood, user)
|
||||||
|
day_log = mood[:day_log] if interactive
|
||||||
|
|
||||||
|
description = [ date_longue(date), mode&.label, libelle_statut(status), ("journal écrit" if day_log) ].compact.join(", ")
|
||||||
|
classes = [ "jour", "jour-#{status}" ]
|
||||||
|
classes << "jour-journal" if day_log
|
||||||
|
classes << "jour-aujourdhui" if date == Date.current
|
||||||
|
options = { class: classes, style: ("--humeur: #{mode.color}" if mode), title: description }
|
||||||
|
|
||||||
|
if interactive && status != :future
|
||||||
|
tag.button(type: "button", **options,
|
||||||
|
tabindex: selected ? 0 : -1,
|
||||||
|
"aria-label": description,
|
||||||
|
"aria-pressed": selected.to_s,
|
||||||
|
data: {
|
||||||
|
day: date.iso8601,
|
||||||
|
date: date_longue(date),
|
||||||
|
mode: mode&.label,
|
||||||
|
statut: libelle_statut(status),
|
||||||
|
couleur: mode&.color,
|
||||||
|
image: (mode || Mode.new).image_url,
|
||||||
|
info: day_log&.info,
|
||||||
|
action: "calendrier#choisir keydown->calendrier#naviguer"
|
||||||
|
})
|
||||||
|
else
|
||||||
|
tag.span(**options, "aria-hidden": true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
56
app/javascript/controllers/calendrier_controller.js
Normal file
56
app/javascript/controllers/calendrier_controller.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Calendrier des humeurs : choisir un jour affiche son détail (illustration, mode, journal).
|
||||||
|
// Au clavier, les flèches se déplacent d'un jour (haut, bas) ou d'une semaine (gauche, droite).
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "carte", "image", "date", "mode", "statut", "journal", "lien" ]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const selectionne = this.element.querySelector(".jour[aria-pressed='true']")
|
||||||
|
if (selectionne && new URLSearchParams(window.location.search).has("day")) {
|
||||||
|
selectionne.scrollIntoView({ block: "center" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
choisir(event) {
|
||||||
|
this.selectionner(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
naviguer(event) {
|
||||||
|
const decalages = { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -7, ArrowRight: 7 }
|
||||||
|
const decalage = decalages[event.key]
|
||||||
|
if (!decalage) return
|
||||||
|
|
||||||
|
const date = new Date(`${event.currentTarget.dataset.day}T12:00:00`)
|
||||||
|
date.setDate(date.getDate() + decalage)
|
||||||
|
const cible = this.element.querySelector(`button.jour[data-day="${date.toISOString().slice(0, 10)}"]`)
|
||||||
|
if (!cible) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.selectionner(cible)
|
||||||
|
cible.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionner(jour) {
|
||||||
|
const precedent = this.element.querySelector(".jour[aria-pressed='true']")
|
||||||
|
if (precedent) {
|
||||||
|
precedent.setAttribute("aria-pressed", "false")
|
||||||
|
precedent.tabIndex = -1
|
||||||
|
}
|
||||||
|
jour.setAttribute("aria-pressed", "true")
|
||||||
|
jour.tabIndex = 0
|
||||||
|
|
||||||
|
const { day, date, mode, statut, couleur, image, info } = jour.dataset
|
||||||
|
this.carteTarget.style.setProperty("--humeur", couleur || "")
|
||||||
|
this.imageTarget.src = image
|
||||||
|
this.dateTarget.textContent = date
|
||||||
|
this.modeTarget.textContent = mode || "Pas de tasse posée"
|
||||||
|
this.statutTarget.textContent = mode ? (statut || "") : ""
|
||||||
|
this.journalTarget.textContent = info || this.journalTarget.dataset.vide
|
||||||
|
this.journalTarget.classList.toggle("detail-journal-vide", !info)
|
||||||
|
this.lienTarget.href = `/day_logs/edit?day=${day}`
|
||||||
|
this.lienTarget.textContent = info ? "Modifier le journal" : "Écrire dans le journal"
|
||||||
|
|
||||||
|
history.replaceState({}, "", `?day=${day}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/javascript/controllers/choix_mode_controller.js
Normal file
12
app/javascript/controllers/choix_mode_controller.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Formulaire du journal : l'illustration suit le mode choisi.
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "carte", "image" ]
|
||||||
|
|
||||||
|
changer(event) {
|
||||||
|
const { image, couleur } = event.currentTarget.dataset
|
||||||
|
this.imageTarget.src = image
|
||||||
|
this.carteTarget.style.setProperty("--humeur", couleur)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { Controller } from '@hotwired/stimulus'
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = [ "image", "modeDay", "dayLogLink", "dayLogInfo" ];
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.lastTarget = null;
|
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const selectedDay = params.get("day");
|
|
||||||
|
|
||||||
if (selectedDay) {
|
|
||||||
const dayEl = document.querySelector(`[data-day="${selectedDay}"]`);
|
|
||||||
if (dayEl) {
|
|
||||||
dayEl.click();
|
|
||||||
dayEl.scrollIntoView({ block: "center" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const logs = document.querySelector(".logs");
|
|
||||||
if (logs) logs.scrollTop = logs.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDayInfo(event) {
|
|
||||||
const image = this.imageTarget;
|
|
||||||
const modeDay = this.modeDayTarget;
|
|
||||||
const day = event.target.dataset.day;
|
|
||||||
const formattedDay = new Date(day).toLocaleDateString('fr-FR');
|
|
||||||
const modeDayContent = formattedDay + ' : ' + event.target.dataset.mode;
|
|
||||||
image.src = event.target.dataset.image;
|
|
||||||
modeDay.textContent = modeDayContent;
|
|
||||||
this.dayLogLinkTarget.href = `/day_logs/edit?day=${day}`;
|
|
||||||
this.dayLogInfoTarget.textContent = event.target.dataset.info || '';
|
|
||||||
event.target.className = "selected-day " + event.target.dataset.mode;
|
|
||||||
|
|
||||||
history.pushState({}, '', `?day=${day}`);
|
|
||||||
|
|
||||||
if (this.lastTarget) {
|
|
||||||
this.lastTarget.className = "day " + this.lastTarget.dataset.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTarget = event.target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -40,6 +40,17 @@ class User < ApplicationRecord
|
||||||
self.moods.last&.mode || "croisiere"
|
self.moods.last&.mode || "croisiere"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Le jour où l'humeur actuelle a commencé : le plus ancien des derniers
|
||||||
|
# enregistrements consécutifs dans le mode actuel.
|
||||||
|
def current_mode_since
|
||||||
|
since = nil
|
||||||
|
moods.reorder(recorded_at: :desc).pluck(:mode_id, :recorded_at).each do |mode_id, recorded_at|
|
||||||
|
break if since && mode_id != since[:mode_id]
|
||||||
|
since = { mode_id: mode_id, day: recorded_at.to_date }
|
||||||
|
end
|
||||||
|
since&.dig(:day)
|
||||||
|
end
|
||||||
|
|
||||||
def current_day_log
|
def current_day_log
|
||||||
self.day_logs.find_by(day: Date.today)
|
self.day_logs.find_by(day: Date.today)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<nav class="nav" aria-label="Navigation principale">
|
<nav class="nav" aria-label="Navigation principale">
|
||||||
<ul>
|
<ul>
|
||||||
<% if authenticated? %>
|
<% if authenticated? %>
|
||||||
<li><%= lien_nav "Mon tableau de bord", dashboard_path %></li>
|
<li><%= lien_nav "Mes humeurs", dashboard_path %></li>
|
||||||
<li><%= button_to "Se déconnecter", session_path, method: :delete, class: "bouton bouton-petit" %></li>
|
<li><%= button_to "Se déconnecter", session_path, method: :delete, class: "bouton bouton-petit" %></li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li><%= lien_nav "Se connecter", new_session_path, class: "bouton bouton-petit" %></li>
|
<li><%= lien_nav "Se connecter", new_session_path, class: "bouton bouton-petit" %></li>
|
||||||
|
|
|
||||||
10
app/views/application/_flash.html.erb
Normal file
10
app/views/application/_flash.html.erb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<% if flash[:alert].present? || flash[:notice].present? %>
|
||||||
|
<div class="flash">
|
||||||
|
<% if flash[:alert].present? %>
|
||||||
|
<p class="flash-message flash-alerte" role="alert"><%= flash[:alert] %></p>
|
||||||
|
<% end %>
|
||||||
|
<% if flash[:notice].present? %>
|
||||||
|
<p class="flash-message" role="status"><%= flash[:notice] %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
@ -34,14 +34,5 @@
|
||||||
<%= f.submit "Envoyer", class: "bouton" %>
|
<%= f.submit "Envoyer", class: "bouton" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<h2>Bulma aux couleurs de la charte</h2>
|
|
||||||
<div class="buttons">
|
|
||||||
<a class="button is-primary" href="#">is-primary</a>
|
|
||||||
<a class="button is-link" href="#">is-link</a>
|
|
||||||
<a class="button is-light" href="#">is-light</a>
|
|
||||||
</div>
|
|
||||||
<p class="title is-4">Titre Bulma</p>
|
|
||||||
<div class="notification is-danger">Notification is-danger</div>
|
|
||||||
|
|
||||||
<%= render "charte/note", texte: "Une note discrète en fin de page." %>
|
<%= render "charte/note", texte: "Une note discrète en fin de page." %>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
= form_with model: day_log do |f|
|
|
||||||
- @day_log.errors.full_messages.each do |msg|
|
|
||||||
.notification.is-danger= msg
|
|
||||||
|
|
||||||
.field
|
|
||||||
= f.label :info, "Journal", class: "label"
|
|
||||||
.control
|
|
||||||
= f.text_area :info, class: "textarea", rows: 5
|
|
||||||
|
|
||||||
.field
|
|
||||||
= label_tag :mode_id, "Mode", class: "label"
|
|
||||||
.control
|
|
||||||
.select.is-fullwidth
|
|
||||||
= select_tag "day_log[mode_id]",
|
|
||||||
options_from_collection_for_select(Current.user.modes, :id, :label, @mood&.mode_id),
|
|
||||||
include_blank: true
|
|
||||||
|
|
||||||
.field.is-hidden
|
|
||||||
= f.date_field :day
|
|
||||||
|
|
||||||
.field.is-grouped
|
|
||||||
.control
|
|
||||||
= f.submit "Enregistrer", class: "button is-primary"
|
|
||||||
.control
|
|
||||||
= link_to "Annuler", dashboard_path(day: @day_log.day), class: "button is-light"
|
|
||||||
44
app/views/day_logs/edit.html.erb
Normal file
44
app/views/day_logs/edit.html.erb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<% titre = @day_log.day ? date_longue(@day_log.day).capitalize : "Journal" %>
|
||||||
|
<% content_for :title, "#{titre} – Kluk" %>
|
||||||
|
<% mode_id = params.dig(:day_log, :mode_id).presence&.to_i || @mood&.mode_id %>
|
||||||
|
<% mode = Current.user.modes.find { |m| m.id == mode_id } %>
|
||||||
|
|
||||||
|
<div class="page-jour" data-controller="choix-mode">
|
||||||
|
<header class="page-jour-entete">
|
||||||
|
<p class="retour"><%= link_to "Retour à mes humeurs", dashboard_path(day: @day_log.day) %></p>
|
||||||
|
<h1 class="page-jour-titre"><%= titre %></h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<figure class="carte-humeur" data-choix-mode-target="carte" style="<%= "--humeur: #{mode.color}" if mode %>">
|
||||||
|
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { choix_mode_target: "image" } %>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<%= form_with model: @day_log, class: "formulaire" do |f| %>
|
||||||
|
<%= render "charte/formulaire_erreur", messages: @day_log.errors.full_messages %>
|
||||||
|
<%= f.hidden_field :day %>
|
||||||
|
|
||||||
|
<fieldset class="champ">
|
||||||
|
<legend>Mode du jour</legend>
|
||||||
|
<ul class="choix-modes">
|
||||||
|
<% Current.user.modes.each do |m| %>
|
||||||
|
<li>
|
||||||
|
<%= radio_button_tag "day_log[mode_id]", m.id, m.id == mode_id,
|
||||||
|
data: { image: m.image_url, couleur: m.color, action: "choix-mode#changer" } %>
|
||||||
|
<%= label_tag "day_log_mode_id_#{m.id}" do %>
|
||||||
|
<span class="pastille" style="--humeur: <%= m.color %>"></span><%= m.label %>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<%= render "charte/champ", form: f, attribut: :info, libelle: "Journal" do %>
|
||||||
|
<%= f.text_area :info, rows: 8, placeholder: "Ce qui a compté ce jour-là…" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<%= f.submit "Enregistrer", class: "bouton" %>
|
||||||
|
<%= link_to "Annuler", dashboard_path(day: @day_log.day) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
.columns.m-auto
|
|
||||||
.current-day.column.is-hidden-mobile
|
|
||||||
%section.m-4
|
|
||||||
%figure.image.has-ratio
|
|
||||||
= image_tag @mood ? @mood.mode.image_url : "unknown.jpg"
|
|
||||||
|
|
||||||
%section.section.column.has-background-primary-light
|
|
||||||
%h1.title.is-4
|
|
||||||
%span.icon
|
|
||||||
%i.fa-regular.fa-calendar
|
|
||||||
%span= l @day_log.day, format: :long
|
|
||||||
|
|
||||||
= render 'form', day_log: @day_log
|
|
||||||
1
app/views/day_logs/new.html.erb
Normal file
1
app/views/day_logs/new.html.erb
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<%= render template: "day_logs/edit" %>
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
%h1 Nouveau journal
|
|
||||||
|
|
||||||
= render 'form', day_log: @day_log
|
|
||||||
|
|
@ -1,26 +1,16 @@
|
||||||
<article class="texte">
|
<% content_for :title, "Bienvenue – Kluk" %>
|
||||||
<div>
|
<article class="texte texte-etroit">
|
||||||
<h1>Set up your password</h1>
|
<%= render "charte/tasses" %>
|
||||||
<p>Please set your password to activate your account.</p>
|
<h1>Bienvenue</h1>
|
||||||
|
<p>Choisissez un mot de passe pour activer votre compte et retrouver les humeurs de votre Kluk.</p>
|
||||||
|
|
||||||
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch do |form| %>
|
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch, class: "formulaire" do |form| %>
|
||||||
<div>
|
<%= render "charte/champ", form: form, attribut: :password, libelle: "Mot de passe" do %>
|
||||||
<%= form.password_field :password,
|
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
|
||||||
required: true,
|
<% end %>
|
||||||
autocomplete: "new-password",
|
<%= render "charte/champ", form: form, attribut: :password_confirmation, libelle: "Le même, une seconde fois" do %>
|
||||||
placeholder: "Enter new password" %>
|
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", maxlength: 72 %>
|
||||||
</div>
|
<% end %>
|
||||||
|
<%= form.submit "Activer mon compte", class: "bouton" %>
|
||||||
<div>
|
|
||||||
<%= form.password_field :password_confirmation,
|
|
||||||
required: true,
|
|
||||||
autocomplete: "new-password",
|
|
||||||
placeholder: "Repeat new password" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<%= form.submit "Activate Account" %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@
|
||||||
<%= preload_link_tag "young-serif-400.woff2" %>
|
<%= preload_link_tag "young-serif-400.woff2" %>
|
||||||
<%= preload_link_tag "atkinson-hyperlegible-next-400.woff2" %>
|
<%= preload_link_tag "atkinson-hyperlegible-next-400.woff2" %>
|
||||||
|
|
||||||
<script src="https://kit.fontawesome.com/079b06296f.js" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
@ -32,6 +30,7 @@
|
||||||
<a class="evitement" href="#contenu">Aller au contenu</a>
|
<a class="evitement" href="#contenu">Aller au contenu</a>
|
||||||
<%= render "entete" %>
|
<%= render "entete" %>
|
||||||
<main id="contenu" tabindex="-1">
|
<main id="contenu" tabindex="-1">
|
||||||
|
<%= render "flash" %>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</main>
|
</main>
|
||||||
<%= render "pied" %>
|
<%= render "pied" %>
|
||||||
|
|
|
||||||
30
app/views/moods/_calendrier.html.erb
Normal file
30
app/views/moods/_calendrier.html.erb
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<%# Calendrier des humeurs « à la GitHub » : une colonne par semaine, une ligne par jour,
|
||||||
|
regroupé par mois. Par défaut, un bloc par année, la plus récente d'abord ;
|
||||||
|
avec par_annee: false, tous les mois à la suite (l'année est ajoutée aux mois d'une autre année). %>
|
||||||
|
<%# locals: (history:, user:, interactive: false, selected_date: nil, par_annee: true) -%>
|
||||||
|
<% blocs = par_annee ? history_by_year(history) : [ [ nil, history ] ] %>
|
||||||
|
<div class="calendrier-humeurs">
|
||||||
|
<% blocs.each do |year, months| %>
|
||||||
|
<%= tag.section class: "annee", "aria-labelledby": ("annee-#{year}" if year) do %>
|
||||||
|
<% if year %>
|
||||||
|
<h2 class="annee-titre" id="annee-<%= year %>"><%= year %></h2>
|
||||||
|
<% end %>
|
||||||
|
<div class="annee-mois">
|
||||||
|
<div class="semaine-jours" aria-hidden="true">
|
||||||
|
<span>lun.</span><span></span><span>mer.</span><span></span><span>ven.</span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<% months.each do |month| %>
|
||||||
|
<div class="mois">
|
||||||
|
<h3 class="mois-nom"><%= l(month[:month], format: year || month[:month].year == Date.current.year ? "%B" : "%B %Y") %></h3>
|
||||||
|
<div class="mois-jours">
|
||||||
|
<% month[:weeks].flatten.each do |mood| %>
|
||||||
|
<%= case_jour(mood, user, interactive: interactive,
|
||||||
|
selected: mood[:recorded_at].to_date == selected_date) %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
18
app/views/moods/_legende.html.erb
Normal file
18
app/views/moods/_legende.html.erb
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<%# locals: (user:, journal: false) -%>
|
||||||
|
<div class="legende">
|
||||||
|
<ul class="legende-modes" aria-label="Les modes">
|
||||||
|
<% user.modes.each do |mode| %>
|
||||||
|
<li><span class="pastille" style="--humeur: <%= mode.color %>"></span><%= mode.label %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<ul class="legende-etats" aria-label="Les autres cases">
|
||||||
|
<% if user.guess? %>
|
||||||
|
<li><span class="pastille pastille-supposee"></span>Mode supposé, pas de tasse posée ce jour-là</li>
|
||||||
|
<% else %>
|
||||||
|
<li><span class="pastille pastille-inconnue"></span>Pas de tasse posée</li>
|
||||||
|
<% end %>
|
||||||
|
<% if journal %>
|
||||||
|
<li><span class="pastille pastille-journal"></span>Journal écrit</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
@ -1,63 +1,31 @@
|
||||||
<div data-controller="mood" class="columns m-auto">
|
<% content_for :title, "Mes humeurs – Kluk" %>
|
||||||
<div class="current-day column is-hidden-mobile">
|
<% history = @user.history %>
|
||||||
<section class="m-4">
|
<% selected_date = (Date.parse(params[:day]) rescue Date.current) %>
|
||||||
<figure class="image has-ratio">
|
<% selected = find_day(history, selected_date) || find_day(history, selected_date = Date.current) %>
|
||||||
<%= image_tag(@user.current_mode.image_url, "data-mood-target": "image") %>
|
<% status = day_status(selected, @user) if selected %>
|
||||||
</figure>
|
<% mode = mode_for(selected, @user) if selected %>
|
||||||
</section>
|
|
||||||
</div>
|
<div class="tableau" data-controller="calendrier">
|
||||||
<section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around">
|
<div class="tableau-calendrier">
|
||||||
<div class="tracker">
|
<h1 class="tableau-titre">Mes humeurs</h1>
|
||||||
<div class="legend is-flex is-flex-wrap-wrap mb-5">
|
<%= render "moods/legende", user: @user, journal: true %>
|
||||||
<div class="is-flex is-align-items-center mr-4">
|
<%= render "moods/calendrier", history: history, user: @user, interactive: true, selected_date: selected_date %>
|
||||||
<div class=""><strong>Légende</strong></div>
|
|
||||||
</div>
|
|
||||||
<% @user.modes.each do |mode| %>
|
|
||||||
<div class="is-flex is-align-items-center mr-4">
|
|
||||||
<div class="bar mr-1" style="<%= style_for_mode(mode, :filled) %>"></div>
|
|
||||||
<div class=""><%= mode.label %></div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="container mb-4 p-3 has-background-white day-info">
|
|
||||||
<div class="title is-4">
|
|
||||||
<span class="icon"><i class="fa-regular fa-calendar"></i></span>
|
|
||||||
<span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span>
|
|
||||||
<%= link_to edit_day_log_for_day_path(day: Date.today), "data-mood-target": "dayLogLink" do %>
|
|
||||||
<span class="icon ml-2"><i class="has-text-grey fa-solid fa-pencil fa-xs"></i></span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<p data-mood-target="dayLogInfo"><%= @user.current_day_log&.info %></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="logs">
|
<aside class="detail" aria-live="polite" aria-label="Jour sélectionné">
|
||||||
<div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3">
|
<figure class="carte-humeur" data-calendrier-target="carte" style="<%= "--humeur: #{mode.color}" if mode %>">
|
||||||
<% @user.history.each do |month| %>
|
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { calendrier_target: "image" } %>
|
||||||
<div class="mr-3">
|
</figure>
|
||||||
<div> <%= I18n.l(month[:month], format: "%b %y").capitalize %></div>
|
<div class="detail-texte">
|
||||||
<div class="is-flex is-flex-wrap-wrap">
|
<h2 class="detail-date" data-calendrier-target="date"><%= date_longue(selected_date) %></h2>
|
||||||
<% month[:weeks].each do |week| %>
|
<p class="detail-mode">
|
||||||
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
|
<span data-calendrier-target="mode"><%= mode&.label || "Pas de tasse posée" %></span>
|
||||||
<% week.each do |mood| %>
|
<span class="detail-statut" data-calendrier-target="statut"><%= libelle_statut(status) if mode %></span>
|
||||||
<% status = day_status(mood, @user) %>
|
</p>
|
||||||
<% mode = mode_for(mood, @user) %>
|
<% info = selected&.dig(:day_log)&.info %>
|
||||||
<div data-image="<%= mode.image_url %>"
|
<p class="detail-journal <%= "detail-journal-vide" unless info %>" data-calendrier-target="journal" data-vide="Rien d’écrit ce jour-là."><%= info || "Rien d’écrit ce jour-là." %></p>
|
||||||
data-mode="<%= mode.label %>"
|
<%= link_to info ? "Modifier le journal" : "Écrire dans le journal", edit_day_log_for_day_path(day: selected_date),
|
||||||
data-day="<%= mood[:recorded_at].to_date.iso8601 %>"
|
class: "bouton bouton-petit", data: { calendrier_target: "lien" } %>
|
||||||
data-info="<%= mood[:day_log]&.info %>"
|
|
||||||
data-action="click->mood#updateDayInfo"
|
|
||||||
title="<%= l(mood[:recorded_at].to_date) %> : <%= mode.label %>"
|
|
||||||
class="<%= css_class_for_day(status, mood[:day_log]) %>"
|
|
||||||
style="<%= style_for_mode(mode, status) %>">
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</aside>
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
30
app/views/moods/public.html.erb
Normal file
30
app/views/moods/public.html.erb
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<% content_for :title, "Comment ça Kluk chez #{@user.username} ?" %>
|
||||||
|
<% mode = @user.moods.last&.mode %>
|
||||||
|
<% since = @user.current_mode_since %>
|
||||||
|
|
||||||
|
<article class="public">
|
||||||
|
<figure class="carte-humeur carte-humeur-grande" style="<%= "--humeur: #{mode.color}" if mode %>">
|
||||||
|
<%= image_tag (mode || Mode.new).image_url, alt: "" %>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="public-texte">
|
||||||
|
<p class="public-qui">Aujourd’hui chez <%= @user.username %></p>
|
||||||
|
<h1 class="public-mode"><%= mode&.label || "Pas encore de tasse posée" %></h1>
|
||||||
|
<% if since %>
|
||||||
|
<p class="public-depuis">
|
||||||
|
<% if since == Date.current %>
|
||||||
|
Depuis ce matin.
|
||||||
|
<% else %>
|
||||||
|
Depuis le <%= l(since, format: since.year == Date.current.year ? "%A %-d %B" : "%-d %B %Y") %>.
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<section class="public-historique" aria-labelledby="public-historique-titre">
|
||||||
|
<h2 id="public-historique-titre">Les derniers mois</h2>
|
||||||
|
<%= render "moods/legende", user: @user %>
|
||||||
|
<%= render "moods/calendrier", user: @user, par_annee: false,
|
||||||
|
history: MoodCalendarService.generate_calendar(@user, start_date: 5.months.ago.beginning_of_month, end_date: Date.current) %>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
<article class="texte">
|
<% content_for :title, "Nouveau mot de passe – Kluk" %>
|
||||||
<h1>Update your password</h1>
|
<article class="texte texte-etroit">
|
||||||
|
<h1>Nouveau mot de passe</h1>
|
||||||
|
|
||||||
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
<%= form_with url: password_path(params[:token]), method: :put, class: "formulaire" do |form| %>
|
||||||
|
<%= render "charte/champ", form: form, attribut: :password, libelle: "Nouveau mot de passe" do %>
|
||||||
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
|
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
|
||||||
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
|
<% end %>
|
||||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
|
<%= render "charte/champ", form: form, attribut: :password_confirmation, libelle: "Le même, une seconde fois" do %>
|
||||||
<%= form.submit "Save" %>
|
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", maxlength: 72 %>
|
||||||
|
<% end %>
|
||||||
|
<%= form.submit "Enregistrer le mot de passe", class: "bouton" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
<article class="texte">
|
<% content_for :title, "Mot de passe oublié – Kluk" %>
|
||||||
<h1>Forgot your password?</h1>
|
<article class="texte texte-etroit">
|
||||||
|
<h1>Mot de passe oublié</h1>
|
||||||
|
<p>Indiquez l’adresse mail de votre compte : vous recevrez un lien pour choisir un nouveau mot de passe.</p>
|
||||||
|
|
||||||
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
<%= form_with url: passwords_path, class: "formulaire" do |form| %>
|
||||||
|
<%= render "charte/champ", form: form, attribut: :email_address, libelle: "Adresse mail" do %>
|
||||||
<%= form_with url: passwords_path do |form| %>
|
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", value: params[:email_address] %>
|
||||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
<% end %>
|
||||||
<%= form.submit "Email reset instructions" %>
|
<%= form.submit "Recevoir le lien", class: "bouton" %>
|
||||||
|
<p class="formulaire-secours"><%= link_to "Retour à la connexion", new_session_path %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
16
app/views/sessions/new.html.erb
Normal file
16
app/views/sessions/new.html.erb
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<% content_for :title, "Connexion – Kluk" %>
|
||||||
|
<article class="texte texte-etroit">
|
||||||
|
<%= render "charte/tasses" %>
|
||||||
|
<h1>Connexion</h1>
|
||||||
|
|
||||||
|
<%= form_with url: session_path, class: "formulaire" do |form| %>
|
||||||
|
<%= render "charte/champ", form: form, attribut: :email_address, libelle: "Adresse mail" do %>
|
||||||
|
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", value: params[:email_address] %>
|
||||||
|
<% end %>
|
||||||
|
<%= render "charte/champ", form: form, attribut: :password, libelle: "Mot de passe" do %>
|
||||||
|
<%= form.password_field :password, required: true, autocomplete: "current-password", maxlength: 72 %>
|
||||||
|
<% end %>
|
||||||
|
<%= form.submit "Se connecter", class: "bouton" %>
|
||||||
|
<p class="formulaire-secours"><%= link_to "Mot de passe oublié ?", new_password_path %></p>
|
||||||
|
<% end %>
|
||||||
|
</article>
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
.columns.m-auto{"data-controller" => "mood"}
|
|
||||||
.column.is-hidden-mobile
|
|
||||||
%section.m-4
|
|
||||||
%figure.image.has-ratio
|
|
||||||
= image_tag("croisiere.jpg")
|
|
||||||
%section.section.column.has-background-primary-light.is-flex.is-flex-direction-column.is-justify-content-center
|
|
||||||
%h1.title Connexion au tableau de bord de la KLUK
|
|
||||||
.flash.mb-2
|
|
||||||
.has-text-danger= flash[:alert] if flash[:alert]
|
|
||||||
.has-text-info= flash[:notice] if flash[:notice]
|
|
||||||
= form_with url: session_path do |form|
|
|
||||||
.field
|
|
||||||
= form.label :email_address, "Email"
|
|
||||||
.control
|
|
||||||
= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Entrez votre email", value: params[:email_address], class: "input"
|
|
||||||
.field
|
|
||||||
= form.label :password, "Mot de passe"
|
|
||||||
.control
|
|
||||||
= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Entrez votre mot de passe", maxlength: 72, class: "input"
|
|
||||||
.field
|
|
||||||
.control
|
|
||||||
%button.button.is-link Se connecter
|
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -6,7 +6,6 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^1.0.4",
|
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
}
|
}
|
||||||
|
|
@ -320,12 +319,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bulma": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^1.0.4",
|
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css": "sass ./app/assets/stylesheets/application.bulma.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
|
"build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,21 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
pending "add some examples to (or delete) #{__FILE__}"
|
describe "#current_mode_since" do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:croisiere) { create(:mode, user: user) }
|
||||||
|
let(:creatif) { create(:mode, user: user) }
|
||||||
|
|
||||||
|
it "returns nil without moods" do
|
||||||
|
expect(user.current_mode_since).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the first day of the latest run of the current mode" do
|
||||||
|
create(:mood, user: user, mode: croisiere, recorded_at: DateTime.parse("2026-01-10 08:00"))
|
||||||
|
create(:mood, user: user, mode: creatif, recorded_at: DateTime.parse("2026-01-12 08:00"))
|
||||||
|
create(:mood, user: user, mode: creatif, recorded_at: DateTime.parse("2026-01-14 08:00"))
|
||||||
|
|
||||||
|
expect(user.current_mode_since).to eq(Date.parse("2026-01-12"))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,6 @@ braces@^3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
bulma@^1.0.4:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz"
|
|
||||||
integrity sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==
|
|
||||||
|
|
||||||
chokidar@^4.0.0:
|
chokidar@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue