1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

Merge branch 'event-card-refacto' into dev

This commit is contained in:
Sylvain 2022-06-15 12:30:55 +02:00
commit cbe949663e
15 changed files with 399 additions and 89 deletions

View File

@ -2,6 +2,7 @@
## next deploy
- Feature the next event in the event page
- Documentation for installing behind a proxy
- Ability to install behind a proxy
- Improved docker image building time
@ -9,6 +10,8 @@
- Run the docker image with the system user
- During the setup, autoconfigure the main domain
- During the setup, ask to set ALLOW_INSECURE_HTTP if DEFAULT_PROTOCOL was set to http
- Fix a bug: unable to edit an event
- Fix a bug: times are not shown in admin/events monitoring page
- Fix a bug: unable to generate the secret key base during the setup
- Fix a bug: error message during the setup: the input device is not a TTY
- Fix a bug: when Fab-manager was installed as non-root user, unable to compile the assets during the upgrade

View File

@ -1,3 +1,3 @@
#web: bundle exec rails server puma -p $PORT
web: bundle exec rails server puma -p $PORT
worker: bundle exec sidekiq -C ./config/sidekiq.yml
webpack: bin/webpacker-dev-server

View File

@ -0,0 +1,135 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { Event } from '../../models/event';
import FormatLib from '../../lib/format';
declare const Application: IApplication;
interface EventCardProps {
event: Event,
cardType: 'sm' | 'md' | 'lg'
}
export const EventCard: React.FC<EventCardProps> = ({ event, cardType }) => {
const { t } = useTranslation('public');
/**
* Format description to remove HTML tags and set a maximum character count
*/
const formatText = (text: string, count: number) => {
text = text.replace(/(<\/p>|<\/h4>|<\/h5>|<\/h6>|<\/pre>|<\/blockquote>)/g, '\n');
text = text.replace(/<br\s*\/?>/g, '\n');
text = text.replace(/<\/?\w+[^>]*>/g, '');
if (text.length > count) {
text = text.slice(0, count) + '…';
}
text = text.replace(/\n+/g, '<br />');
return text;
};
/**
* Return the formatted localized date of the event
*/
const formatDate = (): string => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
const singleDayEvent = startDate.getFullYear() === endDate.getFullYear() &&
startDate.getMonth() === endDate.getMonth() &&
startDate.getDate() === endDate.getDate();
return singleDayEvent
? t('app.public.home.on_the_date', { DATE: FormatLib.date(event.start_date) })
: t('app.public.home.from_date_to_date', { START: FormatLib.date(event.start_date), END: FormatLib.date(event.end_date) });
};
/**
* Return the formatted localized hours of the event
*/
const formatTime = (): string => {
return event.all_day
? t('app.public.home.all_day')
: t('app.public.home.from_time_to_time', { START: FormatLib.time(event.start_date), END: FormatLib.time(event.end_date) });
};
return (
<div className={`event-card event-card--${cardType}`}>
{event.event_image
? <div className="event-card-picture">
<img src={event.event_image} alt="" />
</div>
: cardType !== 'sm' &&
<div className="event-card-picture">
<i className="fas fa-image"></i>
</div>
}
<div className="event-card-desc">
<header>
<span className={`badge bg-${event.category.slug}`}>{event.category.name}</span>
<p className='title'>{event?.title}</p>
</header>
{cardType !== 'sm' &&
<p dangerouslySetInnerHTML={{ __html: formatText(event.description, cardType === 'md' ? 500 : 400) }}></p>
}
</div>
<div className="event-card-info">
{cardType !== 'md' &&
<p>
{formatDate()}
<span>{formatTime()}</span>
</p>
}
<div className="grid">
{cardType !== 'md' &&
event.event_themes.map(theme => {
return (<div key={theme.name} className="grid-item">
<i className="fa fa-tags"></i>
<h6>{theme.name}</h6>
</div>);
})
}
{(cardType !== 'md' && event.age_range) &&
<div className="grid-item">
<i className="fa fa-users"></i>
<h6>{event.age_range?.name}</h6>
</div>
}
{cardType === 'md' &&
<>
<div className="grid-item">
<i className="fa fa-calendar"></i>
<h6>{formatDate()}</h6>
</div>
<div className="grid-item">
<i className="fa fa-clock"></i>
<h6>{formatTime()}</h6>
</div>
</>
}
<div className="grid-item">
<i className="fa fa-user"></i>
{event.nb_free_places > 0 && <h6>{t('app.public.home.still_available') + event.nb_free_places}</h6>}
{event.nb_total_places > 0 && event.nb_free_places <= 0 && <h6>{t('app.public.home.event_full')}</h6>}
{!event.nb_total_places && <h6>{t('app.public.home.without_reservation')}</h6>}
</div>
<div className="grid-item">
<i className="fa fa-bookmark"></i>
{event.amount === 0 && <h6>{t('app.public.home.free_admission')}</h6>}
{event.amount > 0 && <h6>{t('app.public.home.full_price') + FormatLib.price(event.amount)}</h6>}
</div>
</div>
</div>
</div>
);
};
const EventCardWrapper: React.FC<EventCardProps> = ({ event, cardType }) => {
return (
<Loader>
<EventCard event={event} cardType={cardType} />
</Loader>
);
};
Application.Components.component('eventCard', react2angular(EventCardWrapper, ['event', 'cardType']));

View File

@ -72,6 +72,7 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
// reinitialize results datasets
$scope.page = 1;
$scope.eventsGroupByMonth = {};
$scope.featuredEevent = null;
$scope.events = [];
$scope.monthOrder = [];
$scope.noMoreResults = false;
@ -94,6 +95,16 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
*/
$scope.onSingleDay = function (event) { moment(event.start_date).isSame(event.end_date, 'day'); };
/**
* Move down the viewport to the featured event
*/
$scope.scrollToFeaturedEvent = function () {
const card = document.getElementsByClassName('featured-event')[0];
if (card) {
card.childNodes[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
/* PRIVATE SCOPE */
/**
@ -117,7 +128,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
});
});
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth);
return $scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
$scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
$scope.featuredEevent = _.minBy(events.filter(e => moment(e.start_date).isAfter()), e => e.start_date);
}
};

View File

@ -29,6 +29,7 @@
@import "modules/base/fab-text-editor";
@import "modules/base/labelled-input";
@import "modules/calendar/calendar";
@import "modules/events/event";
@import "modules/form/form-input";
@import "modules/form/form-item";
@import "modules/form/form-rich-text";

View File

@ -0,0 +1,202 @@
.event {
&-card {
border: 1px solid var(--gray-soft-dark);
border-radius: 5px;
overflow: hidden;
color: var(--gray-hard-darkest);
&:hover {
color: var(--gray-hard-darkest);
border-color: var(--gray-hard);
cursor: pointer;
.event-card-picture,
.event-card-info { border-color: var(--gray-hard); }
}
&-picture {
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
font-size: 8rem;
color: var(--gray-soft-light);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&-desc {
padding: 15px 15px 30px;
header {
margin-bottom: 1rem;
p {
margin: 0;
font-size: 2rem;
font-weight: 900;
line-height: 1.3;
text-transform: uppercase;
}
.badge {
min-width: auto;
max-width: min(16ch, 33%);
white-space: normal;
word-break: break-word;
margin: 0 0 10px 15px;
float: right;
}
}
p { margin: 0; }
}
&-info {
padding: 15px 20px;
& > p {
font-size: 1.8rem;
font-weight: 600;
color: var(--main);
span {
display: block;
font-size: 0.75em;
font-weight: 400;
line-height: 1.2;
text-transform: lowercase;
}
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: baseline;
gap: 15px;
&-item {
min-height: 20px;
display: flex;
align-items: center;
i {
width: 16px;
height: 16px;
margin-right: 10px;
font-size: 16px;
text-align: center;
color: var(--main);
}
h6 { margin: 0; }
}
}
}
}
// Card size specific
&-card--sm {
display: grid;
grid-template-rows: max-content 1fr;
grid-template-columns: 1fr max-content;
.event-card-desc {
grid-area: 1/1/2/2;
padding-bottom: 10px;
header {
margin-bottom: 0;
p { font-size: 1.6rem; }
}
}
.event-card-picture {
grid-area: 1/2/3/3;
width: 160px;
display: none;
border-left: 1px solid var(--gray-soft-dark);
@media (min-width: 500px) {
display: flex;
}
}
.event-card-info {
grid-area: 2/1/3/2;
padding: 0 15px 15px;
.grid {
gap: 10px 15px;
&-item i {
width: 12px;
height: 12px;
//margin-right: 6px;
font-size: 12px;
text-align: center;
//color: var(--gray-hard-light);
}
}
}
}
&-card--md {
display: grid;
grid-template-rows: min-content 1fr min-content;
.event-card-picture {
height: 250px;
border-bottom: 1px solid var(--gray-soft-dark);
}
.event-card-info {
margin-top: auto;
padding-bottom: 30px;
border-top: 1px solid var(--gray-soft-dark);
}
}
&-card--lg {
border: 1px solid var(--gray-hard);
.event-card-info {
padding-top: 0;
}
.event-card-picture {
border-bottom: 1px solid var(--gray-hard);
img { max-height: 300px; }
}
@media (min-width: 992px) {
grid-column: span 2;
display: grid;
grid-template-columns: 3fr 2fr;
.event-card-desc { grid-area: 1/1/2/2; }
.event-card-info { grid-area: 2/1/3/2; }
.event-card-picture {
grid-area: 1/2/3/3;
border-bottom: none;
border-left: 1px solid var(--gray-hard);
img { max-height: none; }
}
}
}
// layout specific
&-home {
h4 {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
column-gap: 1rem;
i { margin-right: 1rem;}
}
&-list {
display: grid;
grid-template-columns: 1fr;
gap: 30px;
margin-bottom: 5rem;
@media (min-width: 480px) {
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
}
}
}
&-focus {
display: grid;
margin-bottom: 50px;
@media (min-width: 992px) {
grid-template-columns: repeat(auto-fill, minmax(425px, 1fr));
gap: 15px;
}
}
&-monthList {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 2rem;
@media (min-width: 768px) {
grid-template-columns: repeat(auto-fill, minmax(430px, 1fr));
}
}
}

View File

@ -28,7 +28,7 @@
<!--One day event-->
<span ng-if="(event.start_date | amDateFormat:'LL')==(event.end_date | amDateFormat:'LL')">
{{ 'app.admin.events.on_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
<span ng-if="event.all_day == 'false'">
<span ng-if="event.all_day === false">
{{ 'app.admin.events.from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'app.admin.events.to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}
@ -39,8 +39,8 @@
<span ng-if="(event.start_date | amDateFormat:'LL')!=(event.end_date | amDateFormat:'LL')">
{{'app.admin.events.from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
{{'app.admin.events.to_date' | translate}} {{event.end_date | amDateFormat:'LL'}}
<br ng-if="event.all_day == 'false'"/>
<span ng-if="event.all_day == 'false'">
<br ng-if="event.all_day === false"/>
<span ng-if="event.all_day === false">
{{ 'app.admin.events.from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'app.admin.events.to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}

View File

@ -138,10 +138,10 @@
<label class="v-bottom" translate>{{ 'app.shared.event.all_day' }}</label>
<div class="inline v-top">
<label class="checkbox-inline">
<input type="radio" name="event[all_day]" ng-model="event.all_day" value="true" required/> {{ 'app.shared.buttons.yes' | translate }}
<input type="radio" name="event[all_day]" ng-model="event.all_day" ng-value="true" required/> {{ 'app.shared.buttons.yes' | translate }}
</label>
<label class="checkbox-inline">
<input type="radio" name="event[all_day]" ng-model="event.all_day" value="false"/> {{ 'app.shared.buttons.no' | translate }}
<input type="radio" name="event[all_day]" ng-model="event.all_day" ng-value="false"/> {{ 'app.shared.buttons.no' | translate }}
</label>
</div>
</div>
@ -180,7 +180,7 @@
</span>
</div>
</div>
<div class="m-b row" ng-if="event.all_day =='false'">
<div class="m-b row" ng-if="event.all_day === false">
<div class="col-xs-6">
<label translate>{{ 'app.shared.event.start_time' }}</label>
<div>

View File

@ -40,41 +40,31 @@
</div>
</div>
<div class="event-focus" ng-if="featuredEevent && (!currentUser || currentUser.role === 'member')">
<event-card style="display: contents"
event="featuredEevent"
card-type="'lg'"
ui-sref="app.public.events_show({id: featuredEevent.id})">
</event-card>
</div>
<div ng-if="isAuthorized(['admin', 'manager'])">
<button class="btn btn-default" ng-click="scrollToFeaturedEvent()" translate>{{ 'app.public.events_list.show_featured' }}</button>
</div>
<div ng-repeat="month in monthOrder">
<h1>{{monthNames[month.split(',')[0] - 1]}}, {{month.split(',')[1]}}</h1>
<div class="month-events-list" ng-repeat="event in (eventsGroupByMonth[month].length/3 | array)">
<a class="Event" ng-repeat="event in eventsGroupByMonth[month].slice(3*$index, 3*$index + 3)" ui-sref="app.public.events_show({id: event.id})">
<div class="Event-desc">
<h5 class="text-xs m-t-n">{{event.category.name}}</h5>
<h4 class="m-n text-sm clear l-n">{{event.title}}</h4>
<h3 class="m-n" ng-show="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}}</h3>
<h3 class="m-n" ng-hide="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}} <span class="text-sm font-thin" translate> {{ 'app.public.events_list.to_date' }} </span> {{event.end_date | amDateFormat:'L'}}</h3>
<h6 class="m-n" ng-if="!event.amount" translate>{{ 'app.public.events_list.free_admission' }}</h6>
<h6 class="m-n" ng-if="event.amount">{{ 'app.public.events_list.full_price_' | translate }} {{event.amount | currency}} <span ng-repeat="price in event.prices">/ {{ price.category.name }} {{price.amount | currency}}</span></h6>
<div>
<span class="text-black-light text-xs m-r-xs" ng-repeat="theme in event.event_themes">
<i class="fa fa-tags" aria-hidden="true"></i> {{theme.name}}
</span>
<span class="text-black-light text-xs" ng-if="event.age_range"><i class="fa fa-users" aria-hidden="true"></i> {{event.age_range.name}}</span>
<div class="event-monthList">
<event-card style="display: contents"
event="event"
ng-repeat="event in eventsGroupByMonth[month]"
card-type="'sm'"
ng-if="isAuthorized(['admin', 'manager']) || event.id !== featuredEevent.id"
ng-class="{'featured-event': event.id === featuredEevent.id}"
ui-sref="app.public.events_show({id: event.id})">
</event-card>
</div>
<div>
<span class="text-black-light text-xs" ng-if="event.nb_free_places > 0">{{event.nb_free_places}} {{ 'app.public.events_list.still_available' | translate }}</span>
<span class="text-black-light text-xs" ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.events_list.sold_out' }}</span>
<span class="text-black-light text-xs" ng-if="event.nb_total_places == -1" translate>{{ 'app.public.events_list.cancelled' }}</span>
<span class="text-black-light text-xs" ng-if="!event.nb_total_places" translate>{{ 'app.public.events_list.without_reservation' }}</span>
</div>
</div>
<!-- Event Image -->
<div class="Event-picture" ng-if="event.event_image">
<img ng-src="{{event.event_image_small}}" title="{{event.title}}">
</div>
</a>
</div>
</div>
<div class="row">
@ -86,5 +76,3 @@
</section>
</section>

View File

@ -6,48 +6,12 @@
</a>
</h4>
<div class="home-events-list" ng-repeat="event in (upcomingEvents.length/3 | array)">
<div class="Event" ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)" ui-sref="app.public.events_show({id: event.id})">
<div class="Event-picture">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!event.event_image" class="img-responsive">
<img ng-if="event.event_image" src="{{event.event_image_medium}}">
</div>
<div class="Event-desc">
<h3>{{event.title}}</h3>
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
<p ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
</div>
<div class="Event-info">
<div class="Event-info-item">
<i class="fa fa-calendar"></i>
<h6 class="" ng-hide="isOneDayEvent(event)">{{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}</h6>
<h6 class="" ng-show="isOneDayEvent(event)">{{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}</h6>
</div>
<div class="Event-info-item">
<i class="fas fa-clock"></i>
<h6 class="">
<span ng-if="event.all_day == 'true'" translate>{{ 'app.public.home.all_day' }}</span>
<span ng-if="event.all_day == 'false'">{{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }}</span>
</h6>
</div>
<div class="Event-info-item">
<i class="fa fa-user"></i>
<h6 class="">
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.without_reservation' }}</span>
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
</h6>
</div>
<div class="Event-info-item">
<i class="fa fa-bookmark"></i>
<h6 class="">
<span ng-if="event.amount == 0" translate>{{ 'app.public.home.free_admission' }}</span>
<span ng-if="event.amount > 0">{{ 'app.public.home.full_price' | translate }} {{event.amount | currency}}</span>
</h6>
</div>
</div>
</div>
<div class="event-home-list" ng-repeat="event in (upcomingEvents.length/3 | array)">
<event-card style="display: contents"
event="event"
card-type="'md'"
ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)"
ui-sref="app.public.events_show({id: event.id})">
</event-card>
</div>
</section>

View File

@ -84,6 +84,10 @@ class Event < ApplicationRecord
end
end
def all_day?
availability.start_at.hour.zero?
end
private
def event_recurrence

View File

@ -6,12 +6,12 @@ class EventPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.nil? || (user && !user.admin? && !user.manager?)
scope.includes(:event_image, :event_files, :availability, :category)
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
.where('availabilities.start_at >= ?', DateTime.current)
.order('availabilities.start_at ASC')
.references(:availabilities)
else
scope.includes(:event_image, :event_files, :availability, :category)
scope.includes(:event_image, :event_files, :availability, :category, :event_price_categories, :age_range, :events_event_themes, :event_themes)
.references(:availabilities)
end
end

View File

@ -37,8 +37,8 @@ class EventService
start_at = DateTime.new(start_date.year, start_date.month, start_date.day, 0, 0, 0, start_date.zone)
end_at = DateTime.new(end_date.year, end_date.month, end_date.day, 23, 59, 59, end_date.zone)
else
start_at = DateTime.new(start_date.year, start_date.month, start_date.day, start_time.hour, start_time.min, start_time.sec, start_date.zone)
end_at = DateTime.new(end_date.year, end_date.month, end_date.day, end_time.hour, end_time.min, end_time.sec, end_date.zone)
start_at = DateTime.new(start_date.year, start_date.month, start_date.day, start_time&.hour, start_time&.min, start_time&.sec, start_date.zone)
end_at = DateTime.new(end_date.year, end_date.month, end_date.day, end_time&.hour, end_time&.min, end_time&.sec, end_date.zone)
end
{ start_at: start_at, end_at: end_at }
end

View File

@ -32,7 +32,7 @@ json.end_time event.availability.end_at
json.month t('date.month_names')[event.availability.start_at.month]
json.month_id event.availability.start_at.month
json.year event.availability.start_at.year
json.all_day event.availability.start_at.hour.zero? ? 'true' : 'false'
json.all_day event.all_day?
json.availability do
json.id event.availability.id
json.start_at event.availability.start_at

View File

@ -289,6 +289,7 @@ en:
full_price_: "Full price:"
to_date: "to" #eg. from 01/01 to 01/05
all_themes: "All themes"
show_featured: "Show the featured event"
#details and booking of an event
events_show:
event_description: "Event description"