1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

react coponent: plan-card + extracted scss from stylesheet.rb into themes/

This commit is contained in:
Sylvain 2020-10-29 15:53:29 +01:00
parent f5687f1120
commit 4f877ab05d
11 changed files with 477 additions and 87 deletions

View File

@ -136,3 +136,6 @@ gem 'repost'
gem 'icalendar'
gem 'tzinfo-data'
# compilation of dynamic stylesheets (home page & theme)
gem 'sassc'

View File

@ -336,6 +336,8 @@ GEM
rubyzip (>= 1.3.0)
rubyzip (1.3.0)
safe_yaml (1.0.5)
sassc (2.2.1)
ffi (~> 1.9)
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
@ -476,6 +478,7 @@ DEPENDENCIES
rubocop (~> 0.61.1)
rubyXL
rubyzip (>= 1.3.0)
sassc
seed_dump
sha3
sidekiq (>= 6.0.7)

View File

@ -5,19 +5,25 @@
import React from 'react';
import { react2angular } from 'react2angular';
import { IFilterService } from 'angular';
import moment from "moment";
import moment from 'moment';
import _ from 'lodash'
import { IApplication } from '../models/application';
import { Plan } from '../models/plan';
import { User, UserRole } from '../models/user';
declare var Application: IApplication;
interface PlanCardProps {
plan: Plan,
_t: (key: string, interpolation: object) => string,
user: User,
operator: User,
isSelected: boolean,
onSelectPlan: (plan: Plan) => void,
_t: (key: string, interpolation?: object) => Promise<string>,
$filter: IFilterService
}
const PlanCard: React.FC<PlanCardProps> = ({ plan, _t, $filter }) => {
const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected, _t, $filter }) => {
/**
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
*/
@ -30,6 +36,30 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, _t, $filter }) => {
const duration = (): string => {
return moment.duration(plan.interval_count, plan.interval).humanize();
}
/**
* Check if the user can subscribe to the current plan, for himself
*/
const canSubscribeForMe = (): boolean => {
return operator?.role === UserRole.Member || (operator?.role === UserRole.Manager && user?.id === operator?.id)
}
/**
* Check if the user can subscribe to the current plan, for someone else
*/
const canSubscribeForOther = (): boolean => {
return operator?.role === UserRole.Admin || (operator?.role === UserRole.Manager && user?.id !== operator?.id)
}
/**
* Check it the user has subscribed to this plan or not
*/
const hasSubscribedToThisPlan = (): boolean => {
return user?.subscription?.plan?.id === plan.id;
}
/**
* Callback triggered when the user select the plan
*/
const handleSelectPlan = (): void => {
onSelectPlan(plan);
}
return (
<div>
<h3 className="title">{plan.base_name}</h3>
@ -41,8 +71,26 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, _t, $filter }) => {
</div>
</div>
</div>
{canSubscribeForMe() && <div className="cta-button">
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={!_.isNil(user.subscription)}>
{user && <span>{_t('app.public.plans.i_choose_that_plan')}</span>}
{!user && <span>{_t('app.public.plans.i_subscribe_online')}</span>}
</button>}
{hasSubscribedToThisPlan() && <button className="subscribe-button" disabled>
{ _t('app.public.plans.i_already_subscribed') }
</button>}
</div>}
{canSubscribeForOther() && <div className="cta-button">
<button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={_.isNil(user)}>
<span>{ _t('app.public.plans.i_choose_that_plan') }</span>
</button>
</div>}
</div>
);
}
Application.Components.component('planCard', react2angular(PlanCard, ['plan'], ['_t', '$filter']));
Application.Components.component('planCard', react2angular(PlanCard, ['plan', 'user', 'operator', 'onSelectPlan', 'isSelected'], ['_t', '$filter']));

View File

@ -72,16 +72,27 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* @param plan {Object} The plan to subscribe to
*/
$scope.selectPlan = function (plan) {
if ($scope.isAuthenticated()) {
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
updateCartPrice();
setTimeout(() => {
if ($scope.isAuthenticated()) {
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
updateCartPrice();
} else {
$scope.selectedPlan = null;
}
} else {
$scope.selectedPlan = null;
$scope.login();
}
} else {
$scope.login();
}
$scope.$apply();
}, 50);
};
/**
* Check if the provided plan is currently selected
* @param plan {Object} Resource plan
*/
$scope.isSelected = function (plan) {
return $scope.selectedPlan === plan;
};
/**

View File

@ -0,0 +1,85 @@
import { Plan } from './plan';
export enum UserRole {
Member = 'member',
Manager = 'manager',
Admin = 'admin'
}
export interface User {
id: number,
username: string,
email: string,
group_id: number,
role: UserRole
name: string,
need_completion: boolean,
profile: {
id: number,
first_name: string,
last_name: string,
interest: string,
software_mastered: string,
phone: string,
website: string,
job: string,
tours: Array<string>,
facebook: string,
twitter: string,
google_plus: string,
viadeo: string,
linkedin: string,
instagram: string,
youtube: string,
vimeo: string,
dailymotion: string,
github: string,
echosciences: string,
pinterest: string,
lastfm: string,
flickr: string,
user_avatar: {
id: number,
attachment_url: string
}
},
invoicing_profile: {
id: number,
address: {
id: number,
address: string
},
organization: {
id: number,
name: string,
address: {
id: number,
address: string
}
}
},
statistic_profile: {
id: number,
gender: string,
birthday: Date
},
subscribed_plan: Plan,
subscription: {
id: number,
expired_at: Date,
canceled_at: Date,
stripe: boolean,
plan: {
id: number,
base_name: string,
name: string,
interval: string,
interval_count: number,
amount: number
}
},
training_credits: Array<number>,
machine_credits: Array<{machine_id: number, hours_used: number}>,
last_sign_in_at: Date
}

View File

@ -333,7 +333,10 @@
.cta-button {
margin: 20px 0;
.btn {
.subscribe-button {
@extend .btn;
@extend .rounded;
outline: 0;
font-weight: 600;
font-size: rem-calc(16);

View File

@ -31,25 +31,12 @@
ng-class="{'col-md-12 col-lg-12 b-r':(plansGroup.plans.filter(filterDisabledPlans).length % 2 == 1 && key == plansGroup.plans.filter(filterDisabledPlans).length-1)}"
ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy: '-ui_weight'">
<plan-card plan="plan"></plan-card>
<div class="cta-button" ng-if="isAuthorized('member') || (isAuthorized('manager') && ctrl.member.id === currentUser.id)">
<button class="btn btn-default rounded" ng-click="selectPlan(plan)" ng-if="ctrl.member.subscribed_plan.id != plan.id" ng-disabled="ctrl.member.subscribed_plan" ng-class="{ 'bg-yellow': selectedPlan==plan }">
<span ng-if="ctrl.member" translate>{{ 'app.public.plans.i_choose_that_plan' }}</span>
<span ng-if="!ctrl.member" translate>{{ 'app.public.plans.i_subscribe_online' }}</span>
</button>
<button class="btn btn-warning bg-yellow rounded" ng-if="ctrl.member.subscribed_plan.id == plan.id" ng-disabled="ctrl.member.subscribed_plan.id == plan.id" translate>
{{ 'app.public.plans.i_already_subscribed' }}
</button>
</div>
<div class="cta-button" ng-if="isAuthorized('admin') || (isAuthorized('manager') && ctrl.member.id != currentUser.id)">
<button class="btn btn-default rounded" ng-click="selectPlan(plan)" ng-class="{ 'bg-yellow': selectedPlan==plan }" ng-disabled="!ctrl.member">
<span translate>{{ 'app.public.plans.i_choose_that_plan' }}</span>
</button>
</div>
<plan-card plan="plan"
user="ctrl.member"
operator="currentUser"
on-select-plan="selectPlan"
is-selected="isSelected(plan)">
</plan-card>
<br ng-show="!plan.plan_file_url"> <!-- TODO Refacto with CSS -->
<a ng-href="{{ plan.plan_file_url }}" ng-show="plan.plan_file_url" target="_blank" translate>{{ 'app.public.plans.more_information' }}</a>

View File

@ -15,7 +15,7 @@ class Stylesheet < ApplicationRecord
def rebuild!
if Stylesheet.primary && Stylesheet.secondary && name == 'theme'
update(contents: Stylesheet.css)
update(contents: Stylesheet.theme_css)
elsif name == 'home_page'
update(contents: Stylesheet.home_page_css)
end
@ -29,7 +29,7 @@ class Stylesheet < ApplicationRecord
if Stylesheet.theme
Stylesheet.theme.rebuild!
else
Stylesheet.create!(contents: Stylesheet.css, name: 'theme')
Stylesheet.create!(contents: Stylesheet.theme_css, name: 'theme')
end
end
@ -77,56 +77,10 @@ class Stylesheet < ApplicationRecord
Stylesheet.secondary.paint.brightness <= BRIGHTNESS_LOW_LIMIT ? 'white' : 'black'
end
def self.css # rubocop:disable Metrics/AbcSize
<<~CSS
.bg-red { background-color: #{Stylesheet.primary}; }
.bg-red-dark { background-color: #{Stylesheet.primary}; }
#nav .nav { background-color: #{Stylesheet.primary}; }
#nav .nav > li > a { color: #{Stylesheet.primary_text_color}; }
#nav .nav > li > a:hover, #nav .nav > li > a:focus { background-color: #{Stylesheet.primary_light}; color: #{Stylesheet.primary_text_color}; }
#nav .nav > li > a.active { border-left: 3px solid #{Stylesheet.primary_dark}; background-color: #{Stylesheet.primary_light}; color: #{Stylesheet.primary_text_color}; }
.nav-primary ul.nav > li.menu-spacer { background: linear-gradient(45deg, #{Stylesheet.primary_decoration_color}, transparent); }
.nav-primary .text-bordeau { color: #{Stylesheet.primary_decoration_color}; }
.btn-theme { background-color: #{Stylesheet.primary}; color: #{Stylesheet.primary_text_color}; }
.btn-theme:active, .btn-theme:hover { background-color: #{Stylesheet.primary_dark}; color: #{Stylesheet.primary_text_color}; }
.label-theme { background-color: #{Stylesheet.primary}; color: #{Stylesheet.primary_text_color}; }
.btn-link { color: #{Stylesheet.primary} !important; }
.btn-link:hover { color: #{Stylesheet.primary_dark} !important; }
a { color: #{Stylesheet.primary}; }
.about-page-link a.about-link, .user-profile-nav b.caret { color: #{Stylesheet.primary}; }
.app-generator a, .home-events h4 a, a.reinit-filters, .pricing-panel a, .calendar-url a, .article a, a.project-author, a.dsq-brlink, .alert a, .about-fablab a, a.collected-infos { color: #{Stylesheet.primary}; }
.app-generator a:hover, .home-events h4 a:hover, a.reinit-filters:hover, .pricing-panel a:hover, .calendar-url a:hover, .article a:hover, a.project-author:hover, a.dsq-brlink:hover, .widget .widget-content a:hover, .alert a:hover, .about-fablab a:hover, a.collected-infos:hover { color: #{Stylesheet.primary_dark}; }
.btn.btn-default.reserve-button, .btn.btn-default.show-button, .btn.btn-default.red { color: #{Stylesheet.primary} !important; }
.nav.nav-tabs .uib-tab.nav-item:not(.active) a { color: #{Stylesheet.primary}; }
.article h2, .article h3, .article h5 { color: #{Stylesheet.primary} !important; }
table.table thead tr th a, table.table tbody tr td a:not(.btn) { color: #{Stylesheet.primary}; }
table.table thead tr th a:hover, table.table tbody tr td a:not(.btn):hover { color: #{Stylesheet.primary_dark}; }
a:hover, a:focus { color: #{Stylesheet.primary_dark}; }
h2, h3, h3.red, h5 { color: #{Stylesheet.primary} !important; }
h5:after { background-color: #{Stylesheet.primary}; }
.bg-yellow { background-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
.event:hover { background-color: #{Stylesheet.primary}; color: #{Stylesheet.secondary_text_color}; }
.widget h3 { color: #{Stylesheet.primary}; }
.modal-header h1, .custom-invoice .modal-header h1 { color: #{Stylesheet.primary}; }
.block-link:hover, .fc-toolbar .fc-button:hover, .fc-toolbar .fc-button:active, .fc-toolbar .fc-button.fc-state-active { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color} !important; }
.block-link:hover .user-name { color: #{Stylesheet.secondary_text_color} !important; }
.carousel-control:hover, .carousel-control:focus, .carousel-caption .title a:hover { color: #{Stylesheet.secondary}; }
.well.well-warning { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
.text-yellow { color: #{Stylesheet.secondary} !important; }
.red { color: #{Stylesheet.primary} !important; }
.btn-warning, .editable-buttons button[type=submit].btn-primary { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
.btn-warning:hover, .editable-buttons button[type=submit].btn-primary:hover, .btn-warning:focus, .editable-buttons button[type=submit].btn-primary:focus, .btn-warning.focus, .editable-buttons button.focus[type=submit].btn-primary, .btn-warning:active, .editable-buttons button[type=submit].btn-primary:active, .btn-warning.active, .editable-buttons button.active[type=submit].btn-primary, .open > .btn-warning.dropdown-toggle, .editable-buttons .open > button.dropdown-toggle[type=submit].btn-primary { background-color: #{Stylesheet.secondary_dark} !important; border-color: #{Stylesheet.secondary_dark} !important; color: #{Stylesheet.secondary_text_color}; }
.btn-warning-full { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color} !important; }
.heading .heading-btn a:hover { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
.pricing-panel .content .wrap { border-color: #{Stylesheet.secondary}; }
.pricing-panel .cta-button .btn:hover, .pricing-panel .cta-button .custom-invoice .modal-body .elements li:hover, .custom-invoice .modal-body .elements .pricing-panel .cta-button li:hover { background-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
a.label:hover, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:hover, a.label:focus, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:focus { color: #{Stylesheet.primary}; }
.about-picture { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient( #{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('/about-fablab.jpg') no-repeat; }
.social-icons > div:hover { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
.profile-top { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient(#{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('#{CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'}') no-repeat; }
.profile-top .social-links a:hover { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
section#cookies-modal div.cookies-consent .cookies-actions button.accept { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
CSS
def self.theme_css
template = ERB.new(File.read('app/themes/casemate/style.scss.erb')).result
engine = SassC::Engine.new(template, style: :compressed)
engine.render.presence
end
## ===== HOME PAGE =====
@ -149,7 +103,7 @@ class Stylesheet < ApplicationRecord
end
def self.home_page_css
engine = Sass::Engine.new(home_style, syntax: :scss)
engine = SassC::Engine.new(home_style, style: :compressed)
engine.render.presence || '.home-page {}'
end
end

View File

@ -0,0 +1,295 @@
$primary: <%= Stylesheet.primary %> !default;
$primary-light: <%= Stylesheet.primary_light %> !default;
$primary-dark: <%= Stylesheet.primary_dark %> !default;
$secondary: <%= Stylesheet.secondary %> !default;
$secondary-light: <%= Stylesheet.secondary_light %> !default;
$secondary-dark: <%= Stylesheet.secondary_dark %> !default;
$primary-text-color: <%= Stylesheet.primary_text_color %> !default;
$secondary-text-color: <%= Stylesheet.secondary_text_color %> !default;
$primary-decoration-color: <%= Stylesheet.primary_decoration_color %> !default;
.bg-red {
background-color: $primary;
}
.bg-red-dark {
background-color: $primary;
}
#nav .nav {
background-color: $primary;
}
#nav .nav > li > a {
color: $primary-text-color;
}
#nav .nav > li > a:hover,
#nav .nav > li > a:focus {
background-color: $primary-light;
color: $primary-text-color;
}
#nav .nav > li > a.active {
border-left: 3px solid $primary-dark;
background-color: $primary-light;
color: $primary-text-color;
}
.nav-primary ul.nav > li.menu-spacer {
background: linear-gradient(45deg, $primary-decoration-color, transparent);
}
.nav-primary .text-bordeau {
color: $primary-decoration-color;
}
.btn-theme {
background-color: $primary;
color: $primary-text-color;
}
.btn-theme:active,
.btn-theme:hover {
background-color: $primary-dark;
color: $primary-text-color;
}
.label-theme {
background-color: $primary;
color: $primary-text-color;
}
.btn-link {
color: $primary !important;
}
.btn-link:hover {
color: $primary-dark !important;
}
a {
color: $primary;
}
.about-page-link a.about-link,
.user-profile-nav b.caret {
color: $primary;
}
.app-generator a,
.home-events h4 a,
a.reinit-filters,
.pricing-panel a,
.calendar-url a,
.article a,
a.project-author,
a.dsq-brlink,
.alert a,
.about-fablab a,
a.collected-infos {
color: $primary;
}
.app-generator a:hover,
.home-events h4 a:hover,
a.reinit-filters:hover,
.pricing-panel a:hover,
.calendar-url a:hover,
.article a:hover,
a.project-author:hover,
a.dsq-brlink:hover,
.widget .widget-content a:hover,
.alert a:hover,
.about-fablab a:hover,
a.collected-infos:hover {
color: $primary-dark;
}
.btn.btn-default.reserve-button,
.btn.btn-default.show-button,
.btn.btn-default.red {
color: $primary !important;
}
.nav.nav-tabs .uib-tab.nav-item:not(.active) a {
color: $primary;
}
.article h2,
.article h3,
.article h5 {
color: $primary !important;
}
table.table thead tr th a,
table.table tbody tr td a:not(.btn) {
color: $primary;
}
table.table thead tr th a:hover,
table.table tbody tr td a:not(.btn):hover {
color: $primary-dark;
}
a:hover,
a:focus {
color: $primary-dark;
}
h2,
h3,
h3.red,
h5 {
color: $primary !important;
}
h5:after {
background-color: $primary;
}
.bg-yellow {
background-color: $secondary !important;
color: $secondary-text-color;
}
.event:hover {
background-color: $primary;
color: $secondary-text-color;
}
.widget h3 {
color: $primary;
}
.modal-header h1,
.custom-invoice .modal-header h1 {
color: $primary;
}
.block-link:hover,
.fc-toolbar .fc-button:hover,
.fc-toolbar .fc-button:active,
.fc-toolbar .fc-button.fc-state-active {
background-color: $secondary;
color: $secondary-text-color !important;
}
.block-link:hover .user-name {
color: $secondary-text-color !important;
}
.carousel-control:hover,
.carousel-control:focus,
.carousel-caption .title a:hover {
color: $secondary;
}
.well.well-warning {
border-color: $secondary;
background-color: $secondary;
color: $secondary-text-color;
}
.text-yellow {
color: $secondary !important;
}
.red {
color: $primary !important;
}
.btn-warning,
.editable-buttons button[type=submit].btn-primary {
background-color: $secondary !important;
border-color: $secondary !important;
color: $secondary-text-color;
}
.btn-warning:hover,
.btn-warning:focus,
.btn-warning.focus,
.btn-warning:active,
.btn-warning.active,
.editable-buttons button[type=submit].btn-primary:hover,
.editable-buttons button[type=submit].btn-primary:focus,
.editable-buttons button.focus[type=submit].btn-primary,
.editable-buttons button[type=submit].btn-primary:active,
.editable-buttons button.active[type=submit].btn-primary,
.open > .btn-warning.dropdown-toggle,
.editable-buttons .open > button.dropdown-toggle[type=submit].btn-primary {
background-color: $secondary-dark !important;
border-color: $secondary-dark !important;
color: $secondary-text-color;
}
.btn-warning-full {
border-color: $secondary;
background-color: $secondary;
color: $secondary-text-color !important;
}
.heading .heading-btn a:hover {
background-color: $secondary;
color: $secondary-text-color;
}
.pricing-panel .content .wrap {
border-color: $secondary;
}
.pricing-panel .cta-button .btn:hover,
.pricing-panel .cta-button .custom-invoice .modal-body .elements li:hover,
.custom-invoice .modal-body .elements .pricing-panel .cta-button li:hover {
background-color: $secondary !important;
color: $secondary-text-color;
}
a.label:hover,
a.label:focus,
.form-control.form-control-ui-select .select2-choices a.select2-search-choice:hover,
.form-control.form-control-ui-select .select2-choices a.select2-search-choice:focus {
color: $primary;
}
.about-picture {
background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ),
linear-gradient(<%=Stylesheet.primary_with_alpha(0.78)%>, <%=Stylesheet.primary_with_alpha(0.82)%>),
url('/about-fablab.jpg') no-repeat;
}
.social-icons > div:hover {
background-color: $secondary;
color: $secondary-text-color;
}
.profile-top {
background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ),
linear-gradient(<%=Stylesheet.primary_with_alpha(0.78)%>, <%=Stylesheet.primary_with_alpha(0.82)%>),
url("<%=CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'%>") no-repeat;
}
.profile-top .social-links a:hover {
background-color: $secondary !important;
border-color: $secondary !important;
color: $secondary-text-color;
}
section#cookies-modal div.cookies-consent .cookies-actions button.accept {
background-color: $secondary;
color: $secondary-text-color;
}
.pricing-panel {
.cta-button {
button.subscribe-button {
border-color: $secondary;
&.selected-card {
background-color: $secondary;
}
}
}
}

View File

@ -65,7 +65,7 @@ if member.subscription
json.expired_at member.subscription.expired_at.iso8601
json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at
json.stripe member.subscription.stp_subscription_id.present?
json.plan do
json.plan do # TODO, refactor: duplicates subscribed_plan
json.id member.subscription.plan.id
json.base_name member.subscription.plan.base_name
json.name member.subscription.plan.name
@ -82,4 +82,5 @@ json.machine_credits member.machine_credits do |mc|
json.machine_id mc.creditable_id
json.hours_used mc.users_credits.find_by(user_id: member.id).hours_used
end
# TODO, missing space_credits?
json.last_sign_in_at member.last_sign_in_at.iso8601 if member.last_sign_in_at