Compare commits
50 commits
5aec99bbac
...
cbe73af9bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbe73af9bf | ||
|
|
858c535c3c | ||
|
|
509b0b332d | ||
|
|
385441486e | ||
|
|
56fd287b0f | ||
|
|
86f8b877d8 | ||
|
|
4de10ecba7 | ||
|
|
7888395163 | ||
|
|
fe0e9e2316 | ||
|
|
a9cb61ebda | ||
|
|
04121d44b4 | ||
|
|
3d701baf7f | ||
|
|
1389848cc0 | ||
|
|
bb7328f151 | ||
|
|
a75b9242f1 | ||
|
|
2667360c42 | ||
|
|
0a66906138 | ||
|
|
239ea49251 | ||
|
|
4b55156e21 | ||
|
|
1ca18ed5ec | ||
|
|
c3c475b989 | ||
|
|
7fbc41d332 | ||
|
|
71aaa32d15 | ||
|
|
39f1035125 | ||
|
|
4567f3ef6e | ||
|
|
604d517b2c | ||
|
|
7c9b3990f6 | ||
|
|
ee598f4a3f | ||
|
|
78f60e93b4 | ||
|
|
13e2e49279 | ||
|
|
ad1b1e1c6b | ||
|
|
a5c57322c3 | ||
|
|
7397c4b13d | ||
|
|
9c7e61b55e | ||
|
|
4e25f9936a | ||
|
|
f328f3a10d | ||
|
|
1aee7a3c6e | ||
|
|
ef696971ab | ||
|
|
d2d71468b4 | ||
|
|
415abbe70f | ||
|
|
86435da71c | ||
|
|
266352b468 | ||
|
|
20d006e3c9 | ||
|
|
5ba69fbb54 | ||
|
|
5ef9cafb27 | ||
|
|
7af0d10215 | ||
|
|
37f3ef400d | ||
|
|
43cb410e71 | ||
|
|
976ade253c | ||
|
|
9f40829f20 |
64 changed files with 1947 additions and 331 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -32,3 +32,8 @@
|
|||
|
||||
# Ignore master key for decrypting credentials and more.
|
||||
/config/master.key
|
||||
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
/node_modules
|
||||
|
|
|
|||
9
Gemfile
9
Gemfile
|
|
@ -18,7 +18,7 @@ gem "stimulus-rails"
|
|||
gem "jbuilder"
|
||||
|
||||
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
|
||||
# gem "bcrypt", "~> 3.1.7"
|
||||
gem "bcrypt", "~> 3.1.7"
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
gem "tzinfo-data", platforms: %i[ windows jruby ]
|
||||
|
|
@ -51,6 +51,7 @@ group :development, :test do
|
|||
gem "rubocop-rails-omakase", require: false
|
||||
|
||||
gem "rspec-rails", "~> 8.0.0"
|
||||
gem "factory_bot_rails"
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
|
@ -58,6 +59,7 @@ group :development do
|
|||
gem "web-console"
|
||||
gem "solargraph", require: false
|
||||
gem "solargraph-rails", require: false
|
||||
gem "letter_opener_web"
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
|
@ -65,3 +67,8 @@ group :test do
|
|||
gem "capybara"
|
||||
gem "selenium-webdriver"
|
||||
end
|
||||
|
||||
gem "cssbundling-rails", "~> 1.4"
|
||||
gem "rails-i18n"
|
||||
|
||||
gem "haml"
|
||||
|
|
|
|||
35
Gemfile.lock
35
Gemfile.lock
|
|
@ -77,6 +77,7 @@ GEM
|
|||
ast (2.4.3)
|
||||
backport (1.2.0)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.21)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
|
|
@ -95,9 +96,13 @@ GEM
|
|||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crass (1.0.6)
|
||||
cssbundling-rails (1.4.3)
|
||||
railties (>= 6.0.0)
|
||||
date (3.4.1)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
|
|
@ -110,11 +115,20 @@ GEM
|
|||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.5.6)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 6.1.0)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
haml (7.1.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
importmap-rails (2.1.0)
|
||||
|
|
@ -147,6 +161,17 @@ GEM
|
|||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
actionmailer (>= 6.1)
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
|
|
@ -241,6 +266,9 @@ GEM
|
|||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
|
|
@ -372,6 +400,7 @@ GEM
|
|||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
temple (0.10.4)
|
||||
thor (1.3.2)
|
||||
thruster (0.1.14)
|
||||
thruster (0.1.14-aarch64-linux)
|
||||
|
|
@ -418,16 +447,22 @@ PLATFORMS
|
|||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
brakeman
|
||||
capybara
|
||||
cssbundling-rails (~> 1.4)
|
||||
debug
|
||||
factory_bot_rails
|
||||
haml
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal
|
||||
letter_opener_web
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0.2)
|
||||
rails-i18n
|
||||
rspec-rails (~> 8.0.0)
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
|
|
|
|||
2
Procfile.dev
Normal file
2
Procfile.dev
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
web: env RUBY_DEBUG_OPEN=true bin/rails server
|
||||
css: node_modules/yarn/bin/yarn build:css --watch
|
||||
0
app/assets/builds/.keep
Normal file
0
app/assets/builds/.keep
Normal file
BIN
app/assets/images/en-charge-old.jpg
Normal file
BIN
app/assets/images/en-charge-old.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 44 KiB |
BIN
app/assets/images/unknown.jpg
Normal file
BIN
app/assets/images/unknown.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
41
app/assets/stylesheets/application.bulma.scss
Normal file
41
app/assets/stylesheets/application.bulma.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// @charset "utf-8";
|
||||
|
||||
// Import a Google Font
|
||||
// @import url('https://fonts.googleapis.com/css?family=Nunito:400,700');
|
||||
|
||||
// Import only what you need from Bulma
|
||||
// @import "bulma/sass/utilities/_all.sass";
|
||||
// @import "bulma/sass/base/_all.sass";
|
||||
// @import "bulma/sass/elements/button.sass";
|
||||
// @import "bulma/sass/elements/container.sass";
|
||||
// @import "bulma/sass/elements/title.sass";
|
||||
// @import "bulma/sass/form/_all.sass";
|
||||
// @import "bulma/sass/components/navbar.sass";
|
||||
// @import "bulma/sass/layout/hero.sass";
|
||||
// @import "bulma/sass/layout/section.sass";
|
||||
|
||||
@use 'bulma/bulma';
|
||||
@use 'bulma/sass/utilities/_index.scss' as v;
|
||||
@use './kluk';
|
||||
|
||||
// Set your brand colors
|
||||
// $purple: #8A4D76;
|
||||
// $pink: #FA7C91;
|
||||
// $brown: #757763;
|
||||
// $beige-light: #D0D1CD;
|
||||
// $beige-lighter: #EFF0EB;
|
||||
|
||||
// Update Bulma's global variables
|
||||
// $family-sans-serif: "Nunito", sans-serif;
|
||||
// $grey-dark: $brown;
|
||||
// $grey-light: $beige-light;
|
||||
// $primary: $purple;
|
||||
// $link: $pink;
|
||||
// $widescreen-enabled: false;
|
||||
// $fullhd-enabled: false;
|
||||
|
||||
// Update some of Bulma's component variables
|
||||
// $body-background-color: $beige-lighter;
|
||||
// $control-border-width: 2px;
|
||||
// $input-border-color: transparent;
|
||||
// $input-shadow: none;
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
/*
|
||||
* This is a manifest file that'll be compiled into application.css.
|
||||
*
|
||||
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
||||
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
||||
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
||||
* depending on specificity.
|
||||
*
|
||||
* Consider organizing styles into separate files for maintainability.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
font-family: "Sour Gummy", sans-serif;
|
||||
font-weight: 350;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mode {
|
||||
grid-area: mode;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.tracker {
|
||||
grid-area: tracker;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
margin: 30px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.legend .bar-afond {
|
||||
background: black;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.legend .bar-creatif {
|
||||
background: red;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.legend .bar-frigo {
|
||||
background: gray;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.legend .bar-croisiere {
|
||||
background: green;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.legend .bar-en-charge {
|
||||
background: orange;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.legend .bar-explain {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.legend-mood {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"mode"
|
||||
"tracker";
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mode img {
|
||||
height: 100%;
|
||||
width:100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.tracker {
|
||||
grid-area: tracker;
|
||||
}
|
||||
.title h1 {
|
||||
font-weight: 350;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas: "mode tracker";
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode {
|
||||
height: 100vh;
|
||||
grid-area: mode;
|
||||
}
|
||||
.tracker {
|
||||
grid-area: tracker;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 2fr 1fr 6fr 1fr;
|
||||
grid-template-areas:
|
||||
"title title"
|
||||
"info-day info-day"
|
||||
"moods moods"
|
||||
". legend";
|
||||
background-color: lightgoldenrodyellow;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
gap: 45px;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.info-day {
|
||||
grid-area: info-day;
|
||||
margin: 30px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.mode img {
|
||||
height: 100%;
|
||||
width:100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.title h1 {
|
||||
font-weight: 350;
|
||||
}
|
||||
}
|
||||
.moods {
|
||||
grid-area: moods;
|
||||
margin: 30px;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.legend {
|
||||
grid-area: legend;
|
||||
margin: 4px;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
margin: 30px;
|
||||
}
|
||||
.log {
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
.moods .log .week {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: start;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.moods .log .day {
|
||||
border: 1px;
|
||||
margin: 4px;
|
||||
min-width: 15px;
|
||||
min-height: 15px;
|
||||
}
|
||||
.creatif {
|
||||
background-color: red;
|
||||
}
|
||||
.en-charge {
|
||||
background-color: orange;
|
||||
}
|
||||
.frigo-vide {
|
||||
background-color: grey;
|
||||
}
|
||||
.croisiere {
|
||||
background-color: green;
|
||||
}
|
||||
.afond {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 30px;
|
||||
}
|
||||
116
app/assets/stylesheets/kluk.css
Normal file
116
app/assets/stylesheets/kluk.css
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* This is a manifest file that'll be compiled into application.css.
|
||||
*
|
||||
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
||||
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
||||
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
||||
* depending on specificity.
|
||||
*
|
||||
* Consider organizing styles into separate files for maintainability.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
font-family: "Sour Gummy", sans-serif;
|
||||
font-weight: 350;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#navbar-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
|
||||
.tracker {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.logs {
|
||||
height: 60vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.current-day {
|
||||
background-color: white;
|
||||
position: sticky;
|
||||
top: 64px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.bar-afond {
|
||||
background: black;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.bar-creatif {
|
||||
background: red;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.bar-frigo {
|
||||
background: gray;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.bar-croisiere {
|
||||
background: green;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.bar-en-charge {
|
||||
background: orange;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.day {
|
||||
border: 1px;
|
||||
margin: 4px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.selected-day {
|
||||
border: 2px double white;
|
||||
margin: 4px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.creatif {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border: 2px double grey;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.en-charge {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.frigo-vide {
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.croisiere {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.afond {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 30px;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include Authentication
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
# allow_browser versions: :modern
|
||||
end
|
||||
|
|
|
|||
57
app/controllers/concerns/authentication.rb
Normal file
57
app/controllers/concerns/authentication.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :require_authentication
|
||||
helper_method :authenticated?
|
||||
helper_method :current_user
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def allow_unauthenticated_access(**options)
|
||||
skip_before_action :require_authentication, **options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def authenticated?
|
||||
resume_session
|
||||
end
|
||||
|
||||
def current_user
|
||||
authenticated?&.user
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
resume_session || request_authentication
|
||||
end
|
||||
|
||||
def resume_session
|
||||
Current.session ||= find_session_by_cookie
|
||||
end
|
||||
|
||||
def find_session_by_cookie
|
||||
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
|
||||
end
|
||||
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
redirect_to new_session_path
|
||||
end
|
||||
|
||||
def after_authentication_url
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
|
||||
def start_new_session_for(user)
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
||||
Current.session = session
|
||||
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
||||
end
|
||||
end
|
||||
|
||||
def terminate_session
|
||||
Current.session.destroy
|
||||
cookies.delete(:session_id)
|
||||
end
|
||||
end
|
||||
14
app/controllers/home_controller.rb
Normal file
14
app/controllers/home_controller.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class HomeController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
def index
|
||||
user = User.find_by(username: request.subdomain)
|
||||
if user&.public?
|
||||
@mode = user.current_mood
|
||||
@history = user.history
|
||||
render template: "moods/index"
|
||||
else
|
||||
redirect_to dashboard_path
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/controllers/invitations_controller.rb
Normal file
25
app/controllers/invitations_controller.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
class InvitationsController < ApplicationController
|
||||
before_action :set_user_by_token, only: %i[ edit update ]
|
||||
allow_unauthenticated_access
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
password_params = params.expect(user: [ :password, :password_confirmation ])
|
||||
|
||||
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
|
||||
redirect_to root_path, notice: "Account activated!"
|
||||
else
|
||||
redirect_to invitation_path(params[:token]), alert: "Passwords did not match."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user_by_token
|
||||
@user = User.find_by_invitation_token(params[:token])
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
redirect_to root_path, alert: "Invitation link is invalid or has expired."
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
class MoodsController < ApplicationController
|
||||
def index
|
||||
@mode = Mood.last&.mode || "croisiere"
|
||||
@mood_log = Mood.history_for_a_year || []
|
||||
@mode = Current.user.current_mood
|
||||
@history = Current.user.history
|
||||
end
|
||||
end
|
||||
|
|
|
|||
33
app/controllers/passwords_controller.rb
Normal file
33
app/controllers/passwords_controller.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
class PasswordsController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
before_action :set_user_by_token, only: %i[ edit update ]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
if user = User.find_by(email_address: params[:email_address])
|
||||
PasswordsMailer.reset(user).deliver_later
|
||||
end
|
||||
|
||||
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(params.permit(:password, :password_confirmation))
|
||||
redirect_to new_session_path, notice: "Password has been reset."
|
||||
else
|
||||
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def set_user_by_token
|
||||
@user = User.find_by_password_reset_token!(params[:token])
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
class RfidTagsController < ApplicationController
|
||||
skip_forgery_protection
|
||||
allow_unauthenticated_access
|
||||
|
||||
def create
|
||||
identifier = params.expect(:identifier)
|
||||
rfid_tag = RfidTag.find_or_initialize_by(identifier:)
|
||||
rfid_tag_params = params.expect(rfid_tag: [ :chip_id, :identifier ])
|
||||
rfid_tag = RfidTag.find_or_initialize_by(chip_id: rfid_tag_params[:chip_id], identifier: rfid_tag_params[:identifier])
|
||||
if rfid_tag.new_record?
|
||||
register rfid_tag
|
||||
return
|
||||
|
|
@ -15,13 +16,14 @@ class RfidTagsController < ApplicationController
|
|||
private
|
||||
|
||||
def register(rfid_tag)
|
||||
rfid_tag.user = RfidTag.where(chip_id: rfid_tag.chip_id).first&.user
|
||||
rfid_tag.save!
|
||||
head :created, code: :registered
|
||||
end
|
||||
|
||||
def create_mood(rfid_tag)
|
||||
if rfid_tag.mode
|
||||
Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode)
|
||||
Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode, user: rfid_tag.user)
|
||||
head :created, code: :recorded
|
||||
else
|
||||
head :unprocessable_entity
|
||||
|
|
|
|||
21
app/controllers/sessions_controller.rb
Normal file
21
app/controllers/sessions_controller.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
class SessionsController < ApplicationController
|
||||
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." }
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
if user = User.authenticate_by(params.permit(:email_address, :password))
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
else
|
||||
redirect_to new_session_path, alert: "Try another email address or password."
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
terminate_session
|
||||
redirect_to new_session_path
|
||||
end
|
||||
end
|
||||
|
|
@ -1,2 +1,9 @@
|
|||
module MoodsHelper
|
||||
def mode_for(mood)
|
||||
if params[:guess] == "active"
|
||||
mood[:mode] || mood[:guess] || "unknown"
|
||||
else
|
||||
mood[:mode] || "unknown"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
11
app/javascript/controllers/menu_controller.js
Normal file
11
app/javascript/controllers/menu_controller.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ 'burger', 'navbar' ];
|
||||
|
||||
toggle(event) {
|
||||
[ this.burgerTarget, this.navbarTarget ].forEach((target) => {
|
||||
target.classList.toggle("is-active");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,24 @@
|
|||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "image", "info" ]
|
||||
static targets = [ "image", "modeDay", "modeDayMobile" ]
|
||||
|
||||
updateDayInfo(event) {
|
||||
const image = this.imageTarget;
|
||||
const infoDay = this.infoTarget;
|
||||
const modeDay = this.modeDayTarget;
|
||||
const modeDayMobile = this.modeDayMobileTarget;
|
||||
const modeDayContent = event.target.dataset.day + ' : ' + event.target.dataset.mode;
|
||||
image.src = event.target.dataset.image;
|
||||
infoDay.textContent = event.target.dataset.day;
|
||||
modeDayMobile.textContent = modeDayContent;
|
||||
modeDay.textContent = modeDayContent;
|
||||
event.target.className = "selected-day " + event.target.dataset.mode;
|
||||
}
|
||||
|
||||
removeFeedback(event) {
|
||||
event.target.className = "day " + event.target.dataset.mode;
|
||||
}
|
||||
|
||||
connect() {
|
||||
window.location = "#end";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
app/mailers/passwords_mailer.rb
Normal file
6
app/mailers/passwords_mailer.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class PasswordsMailer < ApplicationMailer
|
||||
def reset(user)
|
||||
@user = user
|
||||
mail subject: "Reset your password", to: user.email_address
|
||||
end
|
||||
end
|
||||
8
app/mailers/user_mailer.rb
Normal file
8
app/mailers/user_mailer.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class UserMailer < ApplicationMailer
|
||||
def invitation_email(user, token)
|
||||
@user = user
|
||||
@invite_url = edit_invitation_url(token: token)
|
||||
|
||||
mail(to: @user.email_address, subject: "Vous êtes invité à rejoindre la kluk")
|
||||
end
|
||||
end
|
||||
4
app/models/current.rb
Normal file
4
app/models/current.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
end
|
||||
|
|
@ -1,45 +1,3 @@
|
|||
class Mood < ApplicationRecord
|
||||
class << self
|
||||
def history_for_a_year
|
||||
history(Date.today - 1.year, Date.today)
|
||||
end
|
||||
|
||||
private
|
||||
def history(from, to)
|
||||
return [] if Mood.count < 1
|
||||
|
||||
first_monday = monday_of_the_week(first_mood_date(from))
|
||||
|
||||
current_date = first_monday
|
||||
current_mode = last_mode_before(current_date)
|
||||
log_mood = []
|
||||
mood_range = Mood.order(:recorded_at).where(recorded_at: (first_monday..to))
|
||||
mood_range.each do |mood|
|
||||
while current_date < mood.recorded_at.to_date do
|
||||
log_mood << [ current_date.to_s, current_mode ]
|
||||
current_date += 1
|
||||
end
|
||||
current_mode = mood.mode
|
||||
end
|
||||
|
||||
while current_date <= to.to_date do
|
||||
log_mood << [ current_date.to_s, current_mode ]
|
||||
current_date += 1
|
||||
end
|
||||
log_mood.each_slice(7).to_a
|
||||
end
|
||||
|
||||
def first_mood_date(from)
|
||||
Mood.order(recorded_at: :asc).where("recorded_at >= ?", from).first.recorded_at.to_date
|
||||
end
|
||||
|
||||
def monday_of_the_week(date)
|
||||
date - date.wday + 1
|
||||
end
|
||||
|
||||
def last_mode_before(date)
|
||||
mood = Mood.where("recorded_at < ?", date).last
|
||||
mood&.mode || "croisiere"
|
||||
end
|
||||
end
|
||||
belongs_to :user
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
class RfidTag < ApplicationRecord
|
||||
belongs_to :user, required: false
|
||||
end
|
||||
|
|
|
|||
3
app/models/session.rb
Normal file
3
app/models/session.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Session < ApplicationRecord
|
||||
belongs_to :user
|
||||
end
|
||||
40
app/models/user.rb
Normal file
40
app/models/user.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class User < ApplicationRecord
|
||||
has_secure_password
|
||||
has_many :sessions, dependent: :destroy
|
||||
has_many :moods
|
||||
|
||||
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
||||
|
||||
generates_token_for :invitation, expires_in: 5.days do
|
||||
invitation_accepted_at
|
||||
end
|
||||
|
||||
def invite_user!(email)
|
||||
invited_user = User.create!(
|
||||
email_address: email,
|
||||
password: SecureRandom.hex(16) # temporary password
|
||||
)
|
||||
|
||||
# Send invitation email using the generated token
|
||||
token = invited_user.generate_token_for(:invitation)
|
||||
UserMailer.invitation_email(invited_user, token).deliver_later
|
||||
|
||||
invited_user
|
||||
end
|
||||
|
||||
def self.find_by_invitation_token(token)
|
||||
find_by_token_for(:invitation, token)
|
||||
end
|
||||
|
||||
def invitation_token
|
||||
generate_token_for(:invitation)
|
||||
end
|
||||
|
||||
def history
|
||||
MoodCalendarService.generate_calendar(moods)
|
||||
end
|
||||
|
||||
def current_mood
|
||||
self.moods.last&.mode || "croisiere"
|
||||
end
|
||||
end
|
||||
78
app/services/mood_calendar_service.rb
Normal file
78
app/services/mood_calendar_service.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# app/services/mood_calendar_service.rb
|
||||
class MoodCalendarService
|
||||
def self.generate_calendar(moods, start_date: nil, end_date: Date.current)
|
||||
# Convertir la relation ActiveRecord en tableau de hash
|
||||
data = moods.order(:recorded_at)
|
||||
.pluck(:mode, :recorded_at)
|
||||
.map { |mode, recorded_at| { mode: mode, recorded_at: recorded_at } }
|
||||
|
||||
|
||||
if data.empty?
|
||||
start_date = Date.current
|
||||
else
|
||||
start_date ||= data.first[:recorded_at].to_date
|
||||
end
|
||||
|
||||
if end_date < (start_date + 5.months)
|
||||
end_date = start_date + 5.months
|
||||
end
|
||||
|
||||
# Convertir en Date si ce sont des DateTime ou Time
|
||||
start_date = start_date.to_date
|
||||
end_date = end_date&.to_date
|
||||
|
||||
# Grouper par jour et garder le plus récent pour chaque jour
|
||||
data_by_date = data.group_by { |d| d[:recorded_at].to_date }
|
||||
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
|
||||
|
||||
# Trouver le dernier mood avant start_date pour initialiser le guess
|
||||
last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
|
||||
.max_by { |d| d[:recorded_at] }
|
||||
&.[](:mode)
|
||||
|
||||
# Générer le tableau complet avec tous les jours
|
||||
complete_data = (start_date..end_date).map do |date|
|
||||
if data_by_date[date]
|
||||
last_mode = data_by_date[date][:mode]
|
||||
data_by_date[date]
|
||||
else
|
||||
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode }
|
||||
end
|
||||
end
|
||||
|
||||
# Regrouper par mois avec semaines commençant au premier lundi
|
||||
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
|
||||
.map do |month_start, month_data|
|
||||
# Trouver le premier lundi du mois (à partir du 1er du mois)
|
||||
first_monday = month_start
|
||||
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
|
||||
|
||||
# Trouver le premier lundi du mois SUIVANT
|
||||
next_month_start = month_start.next_month.beginning_of_month
|
||||
next_first_monday = next_month_start
|
||||
next_first_monday = next_first_monday.next_occurring(:monday) unless next_first_monday.monday?
|
||||
|
||||
# Le dernier jour du mois est le dimanche précédant le premier lundi du mois suivant
|
||||
month_end = next_first_monday - 1.day
|
||||
|
||||
# Créer un hash pour accès rapide aux données de complete_data (qui contient déjà les guess)
|
||||
data_hash = complete_data.index_by { |d| d[:recorded_at].to_date }
|
||||
|
||||
# Générer tous les jours du premier lundi jusqu'au dimanche avant le prochain lundi
|
||||
all_days = (first_monday..month_end).map do |date|
|
||||
# Utiliser directement les données de complete_data qui ont déjà le bon guess
|
||||
data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil }
|
||||
end
|
||||
|
||||
# Grouper par semaines
|
||||
weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) }
|
||||
.sort_by { |week_start, _| week_start }
|
||||
.map { |_, week_moods| week_moods }
|
||||
|
||||
{
|
||||
month: month_start,
|
||||
weeks: weeks
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/views/application/_menu.html.erb
Normal file
27
app/views/application/_menu.html.erb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<nav id="navbar-top" class="navbar has-background-primary" role="navigation" aria-label="main navigation" data-controller="menu">
|
||||
<div class="navbar-brand">
|
||||
<div class="navbar-item is-size-5">Comment ça </div>
|
||||
<div class="navbar-item is-size-3"><strong>KLUK</strong></div>
|
||||
<div class="navbar-item is-size-5">aujourd'hui ?</div>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-menu-target="burger" data-action="menu#toggle">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu" data-menu-target="navbar">
|
||||
<div class="navbar-end">
|
||||
<% if authenticated? %>
|
||||
<div class="navbar-item">
|
||||
<%= button_to "Se déconnecter", session_path, method: :delete, class: "button is-light" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="navbar-item">
|
||||
<%= link_to "Se connecter", new_session_path, class: "button is-light" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
1
app/views/home/index.html.haml
Normal file
1
app/views/home/index.html.haml
Normal file
|
|
@ -0,0 +1 @@
|
|||
%h1 Comment ça KLUK ?
|
||||
24
app/views/invitations/edit.html.erb
Normal file
24
app/views/invitations/edit.html.erb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<div>
|
||||
<h1>Set up your password</h1>
|
||||
<p>Please set your password to activate your account.</p>
|
||||
|
||||
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch do |form| %>
|
||||
<div>
|
||||
<%= form.password_field :password,
|
||||
required: true,
|
||||
autocomplete: "new-password",
|
||||
placeholder: "Enter new password" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.password_field :password_confirmation,
|
||||
required: true,
|
||||
autocomplete: "new-password",
|
||||
placeholder: "Repeat new password" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.submit "Activate Account" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -23,11 +23,12 @@
|
|||
<link href="https://fonts.googleapis.com/css2?family=Delius&family=Sour+Gummy:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||
|
||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= render 'menu' %>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,62 @@
|
|||
<main data-controller="mood">
|
||||
<div class="mode">
|
||||
<%= image_tag(@mode + ".jpg", "data-mood-target": "image") %>
|
||||
<main data-controller="mood" class="columns m-auto">
|
||||
<div class="current-day column">
|
||||
<section class="m-4">
|
||||
<h1 class="title is-3 is-spaced">Comment ça KLUK ?</h1>
|
||||
<div class="is-hidden-tablet mb-4">
|
||||
<div class="" data-mood-target="modeDayMobile">Aujourd'hui</div>
|
||||
</div>
|
||||
<figure class="image is-hidden-mobile has-ratio ">
|
||||
<%= image_tag(@mode + ".jpg", "data-mood-target": "image") %>
|
||||
</figure>
|
||||
</section>
|
||||
</div>
|
||||
<div class="tracker">
|
||||
<div class="title">
|
||||
<h1>Comment ça KLUK aujourd'hui ?</h1>
|
||||
<section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around">
|
||||
<div class="is-hidden-mobile mb-4">
|
||||
<div class="title is-3" data-mood-target="modeDay">Aujourd'hui</div>
|
||||
</div>
|
||||
<div class="info-day" data-mood-target="info">Aujourd'hui</div>
|
||||
<div class="moods">
|
||||
<div class="log">
|
||||
<% @mood_log.each do |week| %>
|
||||
<div class="week">
|
||||
<% week.each do |d| %>
|
||||
<% if d[1] %>
|
||||
<div data-image="<%= asset_path(d[1] + ".jpg") %>" data-mode="<%= d[1] %>" data-day="<%= d[0] %>" data-action="mouseover->mood#updateDayInfo mouseleave->mood#updateDayInfo" title="<%= d[0] %> : <%= d[1] %>" class="day <%= d[1] %>"></div>
|
||||
<% else %>
|
||||
<div class="day"></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="tracker">
|
||||
<div class="legend is-flex is-flex-wrap-wrap mb-5">
|
||||
<div class="is-flex is-align-items-center mr-4">
|
||||
<div class="bar-frigo mr-1"></div>
|
||||
<div class="">Triste</div>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mr-4">
|
||||
<div class="bar-en-charge mr-1"></div>
|
||||
<div class="">En charge</div>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mr-4">
|
||||
<div class="bar-croisiere mr-1"></div>
|
||||
<div class="">Croisiere</div>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mr-4">
|
||||
<div class="bar-creatif mr-1"></div>
|
||||
<div>Creatif</div>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mr-4">
|
||||
<div class="bar-afond mr-1"></div>
|
||||
<div class="">A fond</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logs">
|
||||
<div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3">
|
||||
<% @history.each do |month| %>
|
||||
<div class="mr-3">
|
||||
<div> <%= I18n.l(month[:month], format: "%B %Y").capitalize %></div>
|
||||
<div class="is-flex is-flex-wrap-wrap">
|
||||
<% month[:weeks].each do |week| %>
|
||||
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
|
||||
<% week.each do |mood| %>
|
||||
<% mode = mode_for(mood) %>
|
||||
<div data-image="<%= asset_path(mode + ".jpg") %>" data-mode="<%= mode %>" data-day="<%= l mood[:recorded_at].to_date %>" data-action="click->mood#updateDayInfo mouseover->mood#updateDayInfo mouseleave->mood#removeFeedback" title="<%= mood[:recorded_at] %> : <%= mode %>" class="day <%= mode %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div id="end"></>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="legend-mood">
|
||||
<div class="bar-frigo"></div>
|
||||
<div class="bar-explain">Triste</div>
|
||||
</div>
|
||||
<div class="legend-mood">
|
||||
<div class="bar-en-charge"></div>
|
||||
<div class="bar-explain">En charge</div>
|
||||
</div>
|
||||
<div class="legend-mood">
|
||||
<div class="bar-croisiere"></div>
|
||||
<div class="bar-explain">Croisiere</div>
|
||||
</div>
|
||||
<div class="legend-mood">
|
||||
<div class="bar-creatif"></div>
|
||||
<div class="bar-explain">Créatif</div>
|
||||
</div>
|
||||
<div class="legend-afond">
|
||||
<div class="bar-afond"></div>
|
||||
<div class="bar-explain">A fond</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
9
app/views/passwords/edit.html.erb
Normal file
9
app/views/passwords/edit.html.erb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<h1>Update your password</h1>
|
||||
|
||||
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
||||
|
||||
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
|
||||
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
|
||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
|
||||
<%= form.submit "Save" %>
|
||||
<% end %>
|
||||
8
app/views/passwords/new.html.erb
Normal file
8
app/views/passwords/new.html.erb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<h1>Forgot your password?</h1>
|
||||
|
||||
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
||||
|
||||
<%= form_with url: passwords_path do |form| %>
|
||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
||||
<%= form.submit "Email reset instructions" %>
|
||||
<% end %>
|
||||
4
app/views/passwords_mailer/reset.html.erb
Normal file
4
app/views/passwords_mailer/reset.html.erb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<p>
|
||||
You can reset your password within the next 15 minutes on
|
||||
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
|
||||
</p>
|
||||
2
app/views/passwords_mailer/reset.text.erb
Normal file
2
app/views/passwords_mailer/reset.text.erb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
You can reset your password within the next 15 minutes on this password reset page:
|
||||
<%= edit_password_url(@user.password_reset_token) %>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
<h1>RfidTags#post</h1>
|
||||
<p>Find me in app/views/rfid_tags/post.html.erb</p>
|
||||
22
app/views/sessions/new.html.haml
Normal file
22
app/views/sessions/new.html.haml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
%main.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
|
||||
3
app/views/user_mailer/invitation_email.html.erb
Normal file
3
app/views/user_mailer/invitation_email.html.erb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<h1>You're invited!</h1>
|
||||
<p>Click here to set up your account:</p>
|
||||
<%= link_to "Set up account", @invite_url %>
|
||||
13
bin/dev
13
bin/dev
|
|
@ -1,2 +1,11 @@
|
|||
#!/usr/bin/env ruby
|
||||
exec "./bin/rails", "server", *ARGV
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if gem list --no-installed --exact --silent foreman; then
|
||||
echo "Installing foreman..."
|
||||
gem install foreman
|
||||
fi
|
||||
|
||||
# Default to port 3000 if not specified
|
||||
export PORT="${PORT:-3000}"
|
||||
|
||||
exec foreman start -f Procfile.dev --env /dev/null "$@"
|
||||
|
|
|
|||
|
|
@ -16,12 +16,10 @@ module Moodie
|
|||
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
||||
config.autoload_lib(ignore: %w[assets tasks])
|
||||
|
||||
# Configuration for the application, engines, and railties goes here.
|
||||
#
|
||||
# These settings can be overridden in specific environments using the files
|
||||
# in config/environments, which are processed later.
|
||||
#
|
||||
# config.time_zone = "Central Time (US & Canada)"
|
||||
config.time_zone = "Europe/Paris"
|
||||
config.i18n.default_locale = :fr
|
||||
I18n.available_locales = [ :fr, :en ]
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ Rails.application.configure do
|
|||
# Don't care if the mailer can't send.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
config.action_mailer.delivery_method = :letter_opener
|
||||
config.action_mailer.perform_deliveries = true
|
||||
|
||||
# Make template changes take effect immediately.
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
|
|
@ -69,4 +72,8 @@ Rails.application.configure do
|
|||
|
||||
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
|
||||
# config.generators.apply_rubocop_autocorrect_after_generate!
|
||||
#
|
||||
config.hosts << "localhost.localdomain:3000"
|
||||
config.hosts << "robi.localhost.localdomain:3000"
|
||||
config.hosts << "marie.localhost.localdomain:3000"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
Rails.application.routes.draw do
|
||||
post '/rfid_tags' => 'rfid_tags#create'
|
||||
resource :session
|
||||
resources :passwords, param: :token
|
||||
post "/rfid_tags" => "rfid_tags#create"
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
|
|
@ -10,6 +12,13 @@ Rails.application.routes.draw do
|
|||
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
|
||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||
|
||||
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
|
||||
|
||||
# Defines the root path route ("/")
|
||||
root "moods#index"
|
||||
root "home#index"
|
||||
|
||||
get "/invite/:token", to: "invitations#edit", as: :edit_invitation
|
||||
patch "/invite/:token", to: "invitations#update", as: :invitation
|
||||
|
||||
get "/moods", to: "moods#index", as: :dashboard
|
||||
end
|
||||
|
|
|
|||
11
db/migrate/20260112141054_create_users.rb
Normal file
11
db/migrate/20260112141054_create_users.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class CreateUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :users do |t|
|
||||
t.string :email_address, null: false
|
||||
t.string :password_digest, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :users, :email_address, unique: true
|
||||
end
|
||||
end
|
||||
11
db/migrate/20260112141055_create_sessions.rb
Normal file
11
db/migrate/20260112141055_create_sessions.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class CreateSessions < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :sessions do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.string :ip_address
|
||||
t.string :user_agent
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddInvitationAcceptedAtToUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :users, :invitation_accepted_at, :datetime
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
class AddChipIdAndUsersToRfidTags < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :rfid_tags, :chip_id, :string
|
||||
add_reference :rfid_tags, :user, foreign_key: true
|
||||
end
|
||||
end
|
||||
5
db/migrate/20260127150339_add_user_to_moods.rb
Normal file
5
db/migrate/20260127150339_add_user_to_moods.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddUserToMoods < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_reference :moods, :user, foreign_key: true
|
||||
end
|
||||
end
|
||||
7
db/migrate/20260211102848_add_username_to_users.rb
Normal file
7
db/migrate/20260211102848_add_username_to_users.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class AddUsernameToUsers < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
add_column :users, :username, :string
|
||||
User.update_all(username: "temporary")
|
||||
change_column_null :users, :username, false
|
||||
end
|
||||
end
|
||||
5
db/migrate/20260219114040_add_public_to_users.rb
Normal file
5
db/migrate/20260219114040_add_public_to_users.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddPublicToUsers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :users, :public, :boolean, default: false
|
||||
end
|
||||
end
|
||||
33
db/schema.rb
generated
33
db/schema.rb
generated
|
|
@ -10,19 +10,48 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_08_06_151318) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_02_19_114040) do
|
||||
create_table "moods", force: :cascade do |t|
|
||||
t.string "mode"
|
||||
t.datetime "recorded_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "user_id"
|
||||
t.index ["user_id"], name: "index_moods_on_user_id"
|
||||
end
|
||||
|
||||
create_table "rfid_tags", force: :cascade do |t|
|
||||
t.string "identifier"
|
||||
t.string "mode"
|
||||
t.datetime "created_at", default: -> { "CURRENT_DATE" }, null: false
|
||||
t.datetime "updated_at", default: -> { "CURRENT_DATE" }, null: false
|
||||
t.string "chip_id"
|
||||
t.integer "user_id"
|
||||
t.index ["identifier"], name: "index_rfid_tags_on_identifier"
|
||||
t.index ["user_id"], name: "index_rfid_tags_on_user_id"
|
||||
end
|
||||
|
||||
create_table "sessions", force: :cascade do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.string "ip_address"
|
||||
t.string "user_agent"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["identifier"], name: "index_rfid_tags_on_identifier", unique: true
|
||||
t.index ["user_id"], name: "index_sessions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email_address", null: false
|
||||
t.string "password_digest", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "invitation_accepted_at"
|
||||
t.string "username", null: false
|
||||
t.boolean "public", default: false
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "moods", "users"
|
||||
add_foreign_key "rfid_tags", "users"
|
||||
add_foreign_key "sessions", "users"
|
||||
end
|
||||
|
|
|
|||
513
package-lock.json
generated
Normal file
513
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
{
|
||||
"name": "app",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"sass": "^1.94.2",
|
||||
"yarn": "^1.22.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.94.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
|
||||
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yarn": {
|
||||
"version": "1.22.22",
|
||||
"resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz",
|
||||
"integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"yarn": "bin/yarn.js",
|
||||
"yarnpkg": "bin/yarn.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "app",
|
||||
"private": "true",
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"sass": "^1.94.2",
|
||||
"yarn": "^1.22.22"
|
||||
},
|
||||
"scripts": {
|
||||
"build:css": "sass ./app/assets/stylesheets/application.bulma.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
|
||||
}
|
||||
}
|
||||
7
spec/factories/moods.rb
Normal file
7
spec/factories/moods.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :mood do
|
||||
mode { "croisiere" }
|
||||
recorded_at { DateTime.now }
|
||||
association :user
|
||||
end
|
||||
end
|
||||
6
spec/factories/user.rb
Normal file
6
spec/factories/user.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :user do
|
||||
sequence(:email_address) { |n| "user#{n}@example.com" }
|
||||
password_digest { BCrypt::Password.create('password123') }
|
||||
end
|
||||
end
|
||||
11
spec/fixtures/users.yml
vendored
Normal file
11
spec/fixtures/users.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
<% password_digest = BCrypt::Password.create("password") %>
|
||||
|
||||
one:
|
||||
email_address: one@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
|
||||
two:
|
||||
email_address: two@example.com
|
||||
password_digest: <%= password_digest %>
|
||||
5
spec/models/user_spec.rb
Normal file
5
spec/models/user_spec.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe User, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
|
|
@ -9,21 +9,23 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ
|
|||
# return unless Rails.env.test?
|
||||
require 'rspec/rails'
|
||||
# Add additional requires below this line. Rails is not loaded until this point!
|
||||
require 'factory_bot_rails'
|
||||
|
||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
||||
# run as spec files by default. This means that files in spec/support that end
|
||||
# in _spec.rb will both be required and run as specs, causing the specs to be
|
||||
# run twice. It is recommended that you do not name files matching this glob to
|
||||
# end with _spec.rb. You can configure this pattern with the --pattern
|
||||
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
||||
#
|
||||
# The following line is provided for convenience purposes. It has the downside
|
||||
# of increasing the boot-up time by auto-requiring all files in the support
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
||||
# run as spec files by default. This means that files in spec/support that end
|
||||
# in _spec.rb will both be required and run as specs, causing the specs to be
|
||||
# run twice. It is recommended that you do not name files matching this glob to
|
||||
# end with _spec.rb. You can configure this pattern with the --pattern
|
||||
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
||||
#
|
||||
# The following line is provided for convenience purposes. It has the downside
|
||||
# of increasing the boot-up time by auto-requiring all files in the support
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
|
||||
|
||||
# Ensures that the test database schema matches the current schema file.
|
||||
# If there are pending migrations it will invoke `db:test:prepare` to
|
||||
|
|
|
|||
422
spec/services/mood_calendar_service_spec.rb
Normal file
422
spec/services/mood_calendar_service_spec.rb
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
# spec/services/mood_calendar_service_spec.rb
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MoodCalendarService do
|
||||
describe '.generate_calendar' do
|
||||
context 'avec un recorded_at pour chaque jour' do
|
||||
it 'génère un calendrier complet sans jours nil' do
|
||||
# Données du 1er au 7 février 2025 (une semaine complète)
|
||||
(1..7).each do |day|
|
||||
create(:mood, mode: "mood_#{day}", recorded_at: Time.zone.parse("2025-02-0#{day} 10:00:00"))
|
||||
end
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
expect(result.length).to eq(1) # Un seul mois
|
||||
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
|
||||
|
||||
# Première semaine commence le lundi 3 février
|
||||
first_week = result[0][:weeks][0]
|
||||
expect(first_week.length).to eq(7) # Du lundi 3 au dimanche 9
|
||||
|
||||
# Vérifier que tous les jours du 3 au 7 ont un mode
|
||||
(3..7).each do |day|
|
||||
mood = first_week.find { |m| m[:recorded_at].day == day }
|
||||
expect(mood[:mode]).to eq("mood_#{day}")
|
||||
expect(mood).not_to have_key(:guess)
|
||||
end
|
||||
|
||||
# Le 8 et 9 février devraient avoir mode: nil avec guess: "mood_7"
|
||||
mood_8 = first_week.find { |m| m[:recorded_at].day == 8 }
|
||||
expect(mood_8[:mode]).to be_nil
|
||||
expect(mood_8[:guess]).to eq("mood_7")
|
||||
|
||||
mood_9 = first_week.find { |m| m[:recorded_at].day == 9 }
|
||||
expect(mood_9[:mode]).to be_nil
|
||||
expect(mood_9[:guess]).to eq("mood_7")
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec des jours manquants' do
|
||||
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do
|
||||
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
|
||||
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
|
||||
|
||||
moods = Mood.all
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
moods,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
expect(result.length).to eq(1)
|
||||
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
|
||||
|
||||
# Trouver tous les jours
|
||||
all_days = result[0][:weeks].flatten
|
||||
|
||||
# Le 12 février devrait avoir le dernier mood (content à 14:20)
|
||||
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
|
||||
expect(mood_12[:mode]).to eq("content")
|
||||
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
|
||||
|
||||
# Le 13 février devrait être nil avec guess: "content"
|
||||
mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") }
|
||||
expect(mood_13[:mode]).to be_nil
|
||||
expect(mood_13[:guess]).to eq("content")
|
||||
|
||||
# Le 14 février devrait être nil avec guess: "content"
|
||||
mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") }
|
||||
expect(mood_14[:mode]).to be_nil
|
||||
expect(mood_14[:guess]).to eq("content")
|
||||
|
||||
# Le 15 février devrait avoir le mood "heureux"
|
||||
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
||||
expect(mood_15[:mode]).to eq("heureux")
|
||||
|
||||
# Le 16 février devrait être nil avec guess: "heureux"
|
||||
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
||||
expect(mood_16[:mode]).to be_nil
|
||||
expect(mood_16[:guess]).to eq("heureux")
|
||||
|
||||
# Les jours avant le 12 (à partir du premier lundi 3 février) devraient avoir guess: nil
|
||||
mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") }
|
||||
expect(mood_3[:mode]).to be_nil
|
||||
expect(mood_3[:guess]).to be_nil
|
||||
end
|
||||
|
||||
it 'gère correctement plusieurs mois avec des jours manquants' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
create(:mood, mode: "joyeux", recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-04-30")
|
||||
)
|
||||
|
||||
# Devrait avoir exactement février, mars et avril
|
||||
expect(result.length).to eq(3)
|
||||
|
||||
# Vérifier février
|
||||
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
|
||||
expect(february).not_to be_nil
|
||||
|
||||
# Vérifier mars (tous les jours devraient avoir mode: nil et guess: "heureux")
|
||||
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
||||
expect(march).not_to be_nil
|
||||
all_march_days = march[:weeks].flatten
|
||||
|
||||
# Tous les jours de mars (y compris ceux qui débordent début avril) devraient avoir mode: nil
|
||||
expect(all_march_days.all? { |d| d[:mode].nil? }).to be true
|
||||
|
||||
# Tous les jours de mars devraient avoir guess: "heureux"
|
||||
# (le mois de mars va du 3 mars au 6 avril, avant le mood du 20 avril)
|
||||
all_march_days.each do |day|
|
||||
expect(day[:guess]).to eq("heureux"),
|
||||
"Le jour #{day[:recorded_at].to_date} devrait avoir guess: 'heureux' mais a: #{day[:guess].inspect}"
|
||||
end
|
||||
|
||||
# Vérifier avril
|
||||
april = result.find { |m| m[:month] == Date.parse("2025-04-01") }
|
||||
expect(april).not_to be_nil
|
||||
all_april_days = april[:weeks].flatten
|
||||
mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") }
|
||||
expect(mood_20_april[:mode]).to eq("joyeux")
|
||||
|
||||
# Les jours du 7 au 19 avril devraient avoir guess: "heureux"
|
||||
mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") }
|
||||
expect(mood_10_april[:guess]).to eq("heureux")
|
||||
|
||||
# Les jours après le 20 avril devraient avoir guess: "joyeux"
|
||||
mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") }
|
||||
expect(mood_21_april[:guess]).to eq("joyeux")
|
||||
end
|
||||
end
|
||||
|
||||
context 'sans aucun recorded_at' do
|
||||
it 'retourne un tableau vide' do
|
||||
result = MoodCalendarService.generate_calendar(Mood.none)
|
||||
|
||||
expect(result).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec plusieurs moods le même jour' do
|
||||
it 'garde uniquement le mood le plus récent' do
|
||||
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
|
||||
create(:mood, mode: "neutre", recorded_at: Time.zone.parse("2025-02-12 12:00:00"))
|
||||
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
|
||||
create(:mood, mode: "fatigué", recorded_at: Time.zone.parse("2025-02-12 09:15:00"))
|
||||
|
||||
moods = Mood.all
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
moods,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
all_days = result[0][:weeks].flatten
|
||||
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
|
||||
|
||||
expect(mood_12[:mode]).to eq("content")
|
||||
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'structure du calendrier' do
|
||||
it 'commence chaque mois par le premier lundi' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
february = result[0]
|
||||
first_day = february[:weeks][0][0]
|
||||
|
||||
# Le premier jour de février 2025 devrait être le lundi 3 février
|
||||
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
||||
expect(first_day[:recorded_at].to_date.monday?).to be true
|
||||
end
|
||||
|
||||
it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-03-31")
|
||||
)
|
||||
|
||||
february = result[0]
|
||||
all_days = february[:weeks].flatten
|
||||
|
||||
# Premier jour : lundi 3 février 2025
|
||||
first_day = all_days.first
|
||||
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
||||
expect(first_day[:recorded_at].to_date.monday?).to be true
|
||||
|
||||
# Dernier jour : dimanche 2 mars 2025 (veille du premier lundi de mars qui est le 3)
|
||||
last_day = all_days.last
|
||||
expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
||||
expect(last_day[:recorded_at].to_date.sunday?).to be true
|
||||
|
||||
# Le 3 mars (premier lundi de mars) ne devrait PAS être dans février
|
||||
expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false
|
||||
end
|
||||
|
||||
it 'toutes les semaines ont exactement 7 jours' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
february = result[0]
|
||||
|
||||
# Toutes les semaines devraient avoir exactement 7 jours
|
||||
february[:weeks].each do |week|
|
||||
expect(week.length).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
it 'retourne month comme Date (le premier du mois)' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
february = result[0]
|
||||
|
||||
# month devrait être une Date
|
||||
expect(february[:month]).to be_a(Date)
|
||||
expect(february[:month]).to eq(Date.parse("2025-02-01"))
|
||||
expect(february[:month].day).to eq(1) # Premier du mois
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec paramètres par défaut' do
|
||||
it 'utilise le premier mood comme start_date et aujourd\'hui comme end_date' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(Mood.all)
|
||||
|
||||
expect(result).not_to be_empty
|
||||
|
||||
# Devrait commencer en février
|
||||
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec start_date spécifié' do
|
||||
it 'commence à la date spécifiée' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
start_date: Date.parse("2025-03-01"),
|
||||
end_date: Date.parse("2025-03-31")
|
||||
)
|
||||
|
||||
# Devrait commencer en mars même si le mood est en février
|
||||
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
||||
|
||||
# Ne devrait pas contenir février
|
||||
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false
|
||||
|
||||
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
|
||||
all_march_days = result.first[:weeks].flatten
|
||||
puts all_march_days.inspect
|
||||
expect(all_march_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec end_date spécifié' do
|
||||
it 'se termine à la date spécifiée' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
# Devrait s'arrêter en février
|
||||
expect(result.last[:month]).to eq(Date.parse("2025-02-01"))
|
||||
|
||||
# Ne devrait pas contenir mars ou au-delà
|
||||
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec start_date et end_date spécifiés' do
|
||||
it 'génère le calendrier pour la plage spécifiée' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
start_date: Date.parse("2025-03-01"),
|
||||
end_date: Date.parse("2025-03-31")
|
||||
)
|
||||
|
||||
# Devrait contenir uniquement mars
|
||||
expect(result.length).to eq(1)
|
||||
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
||||
|
||||
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
|
||||
all_days = result.first[:weeks].flatten
|
||||
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
||||
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||
end
|
||||
|
||||
it 'gère une plage de plusieurs mois' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
start_date: Date.parse("2025-02-01"),
|
||||
end_date: Date.parse("2025-04-30")
|
||||
)
|
||||
|
||||
# Devrait contenir février, mars et avril
|
||||
months = result.map { |m| m[:month] }
|
||||
expect(months).to include(
|
||||
Date.parse("2025-02-01"),
|
||||
Date.parse("2025-03-01"),
|
||||
Date.parse("2025-04-01")
|
||||
)
|
||||
end
|
||||
|
||||
it 'fonctionne avec DateTime en paramètres' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
# Passer des DateTime au lieu de Date
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
start_date: Time.zone.parse("2025-02-01 00:00:00"),
|
||||
end_date: Time.zone.parse("2025-02-28 23:59:59")
|
||||
)
|
||||
|
||||
expect(result).not_to be_empty
|
||||
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec une plage ne contenant aucun mood' do
|
||||
it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
start_date: Date.parse("2025-04-01"),
|
||||
end_date: Date.parse("2025-04-30")
|
||||
)
|
||||
|
||||
# Devrait contenir avril
|
||||
expect(result.first[:month]).to eq(Date.parse("2025-04-01"))
|
||||
|
||||
# Tous les jours devraient avoir mode: nil et guess: "heureux"
|
||||
all_days = result.first[:weeks].flatten
|
||||
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
||||
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec plusieurs mois' do
|
||||
it 'chaque mois commence et finit correctement' do
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
|
||||
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
Mood.all,
|
||||
end_date: Date.parse("2025-03-31")
|
||||
)
|
||||
|
||||
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
|
||||
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
||||
|
||||
# Février se termine le dimanche 2 mars
|
||||
last_day_february = february[:weeks].flatten.last
|
||||
expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
||||
|
||||
# Mars commence le lundi 3 mars
|
||||
first_day_march = march[:weeks].flatten.first
|
||||
expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03"))
|
||||
|
||||
# Pas de chevauchement
|
||||
expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date)
|
||||
end
|
||||
end
|
||||
|
||||
context 'avec un scope spécifique' do
|
||||
it 'fonctionne avec des moods filtrés par user' do
|
||||
user1 = create(:user)
|
||||
user2 = create(:user)
|
||||
|
||||
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"), user: user1)
|
||||
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-16 10:00:00"), user: user2)
|
||||
|
||||
moods = Mood.where(user: user1)
|
||||
result = MoodCalendarService.generate_calendar(
|
||||
moods,
|
||||
end_date: Date.parse("2025-02-28")
|
||||
)
|
||||
|
||||
all_days = result[0][:weeks].flatten
|
||||
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
||||
expect(mood_15[:mode]).to eq("heureux")
|
||||
|
||||
# Le mood du user2 ne devrait pas apparaître
|
||||
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
||||
expect(mood_16[:mode]).to be_nil
|
||||
expect(mood_16[:guess]).to eq("heureux")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
spec/support/factory_bot.rb
Normal file
3
spec/support/factory_bot.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
end
|
||||
141
yarn.lock
Normal file
141
yarn.lock
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
|
||||
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
|
||||
|
||||
"@parcel/watcher-linux-x64-musl@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
|
||||
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
|
||||
|
||||
"@parcel/watcher@^2.4.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"
|
||||
integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
is-glob "^4.0.3"
|
||||
micromatch "^4.0.5"
|
||||
node-addon-api "^7.0.0"
|
||||
optionalDependencies:
|
||||
"@parcel/watcher-android-arm64" "2.5.1"
|
||||
"@parcel/watcher-darwin-arm64" "2.5.1"
|
||||
"@parcel/watcher-darwin-x64" "2.5.1"
|
||||
"@parcel/watcher-freebsd-x64" "2.5.1"
|
||||
"@parcel/watcher-linux-arm-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-arm-musl" "2.5.1"
|
||||
"@parcel/watcher-linux-arm64-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-arm64-musl" "2.5.1"
|
||||
"@parcel/watcher-linux-x64-glibc" "2.5.1"
|
||||
"@parcel/watcher-linux-x64-musl" "2.5.1"
|
||||
"@parcel/watcher-win32-arm64" "2.5.1"
|
||||
"@parcel/watcher-win32-ia32" "2.5.1"
|
||||
"@parcel/watcher-win32-x64" "2.5.1"
|
||||
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
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:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
detect-libc@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz"
|
||||
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
|
||||
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
immutable@^5.0.2:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz"
|
||||
integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-number@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
micromatch@^4.0.5:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
dependencies:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
node-addon-api@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz"
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
|
||||
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
||||
|
||||
sass@^1.94.2:
|
||||
version "1.94.2"
|
||||
resolved "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz"
|
||||
integrity sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==
|
||||
dependencies:
|
||||
chokidar "^4.0.0"
|
||||
immutable "^5.0.2"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
optionalDependencies:
|
||||
"@parcel/watcher" "^2.4.1"
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
|
||||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
yarn@^1.22.22:
|
||||
version "1.22.22"
|
||||
resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz"
|
||||
integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==
|
||||
Loading…
Add table
Reference in a new issue