Skip to content
Extraits de code Groupes Projets
Non vérifiée Valider 65fffeac rédigé par Eugen Rochko's avatar Eugen Rochko Validation de GitHub
Parcourir les fichiers

Redesign landing page (#10232)

parent 6a8dc59e
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
Affichage de
avec 460 ajouts et 1012 suppressions
# frozen_string_literal: true # frozen_string_literal: true
class AboutController < ApplicationController class AboutController < ApplicationController
before_action :set_body_classes layout 'public'
before_action :set_instance_presenter, only: [:show, :more, :terms] before_action :set_instance_presenter, only: [:show, :more, :terms]
def show def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @hide_navbar = true
@initial_state_json = serializable_resource.to_json
end end
def more def more; end
render layout: 'public'
end
def terms def terms; end
render layout: 'public'
end
private private
...@@ -28,15 +24,4 @@ class AboutController < ApplicationController ...@@ -28,15 +24,4 @@ class AboutController < ApplicationController
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def set_body_classes
@body_classes = 'with-modals'
end
def initial_state_params
{
settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
token: current_session&.token,
}
end
end end
# frozen_string_literal: true
class PublicTimelinesController < ApplicationController
layout 'public'
before_action :check_enabled
before_action :set_body_classes
before_action :set_instance_presenter
def show
respond_to do |format|
format.html do
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end
end
end
private
def check_enabled
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview
end
def set_body_classes
@body_classes = 'with-modals'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end
...@@ -13,8 +13,10 @@ class TagsController < ApplicationController ...@@ -13,8 +13,10 @@ class TagsController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = ActiveModelSerializers::SerializableResource.new(
@initial_state_json = serializable_resource.to_json InitialStatePresenter.new(settings: {}, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end end
format.rss do format.rss do
...@@ -25,8 +27,7 @@ class TagsController < ApplicationController ...@@ -25,8 +27,7 @@ class TagsController < ApplicationController
end end
format.json do format.json do
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]) @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
.paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render json: collection_presenter, render json: collection_presenter,
...@@ -55,11 +56,4 @@ class TagsController < ApplicationController ...@@ -55,11 +56,4 @@ class TagsController < ApplicationController
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
) )
end end
def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end end
...@@ -56,4 +56,12 @@ module HomeHelper ...@@ -56,4 +56,12 @@ module HomeHelper
'emojify' 'emojify'
end end
end end
def optional_link_to(condition, path, options = {}, &block)
if condition
link_to(path, options, &block)
else
content_tag(:div, &block)
end
end
end end
...@@ -7,7 +7,6 @@ import { hydrateStore } from '../actions/store'; ...@@ -7,7 +7,6 @@ import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline'; import PublicTimeline from '../features/standalone/public_timeline';
import CommunityTimeline from '../features/standalone/community_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline';
import ModalContainer from '../features/ui/containers/modal_container'; import ModalContainer from '../features/ui/containers/modal_container';
import initialState from '../initial_state'; import initialState from '../initial_state';
...@@ -26,24 +25,22 @@ export default class TimelineContainer extends React.PureComponent { ...@@ -26,24 +25,22 @@ export default class TimelineContainer extends React.PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired, locale: PropTypes.string.isRequired,
hashtag: PropTypes.string, hashtag: PropTypes.string,
showPublicTimeline: PropTypes.bool.isRequired, local: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
showPublicTimeline: initialState.settings.known_fediverse, local: !initialState.settings.known_fediverse,
}; };
render () { render () {
const { locale, hashtag, showPublicTimeline } = this.props; const { locale, hashtag, local } = this.props;
let timeline; let timeline;
if (hashtag) { if (hashtag) {
timeline = <HashtagTimeline hashtag={hashtag} />; timeline = <HashtagTimeline hashtag={hashtag} />;
} else if (showPublicTimeline) {
timeline = <PublicTimeline />;
} else { } else {
timeline = <CommunityTimeline />; timeline = <PublicTimeline local={local} />;
} }
return ( return (
...@@ -51,6 +48,7 @@ export default class TimelineContainer extends React.PureComponent { ...@@ -51,6 +48,7 @@ export default class TimelineContainer extends React.PureComponent {
<Provider store={store}> <Provider store={store}>
<Fragment> <Fragment>
{timeline} {timeline}
{ReactDOM.createPortal( {ReactDOM.createPortal(
<ModalContainer />, <ModalContainer />,
document.getElementById('modal-container'), document.getElementById('modal-container'),
......
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../../ui/containers/status_list_container';
import { expandCommunityTimeline } from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { defineMessages, injectIntl } from 'react-intl';
import { connectCommunityStream } from '../../../actions/streaming';
const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
});
export default @connect()
@injectIntl
class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(expandCommunityTimeline());
this.disconnect = dispatch(connectCommunityStream());
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
handleLoadMore = maxId => {
this.props.dispatch(expandCommunityTimeline({ maxId }));
}
render () {
const { intl } = this.props;
return (
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='users'
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
/>
<StatusListContainer
timelineId='community'
onLoadMore={this.handleLoadMore}
scrollKey='standalone_public_timeline'
trackScroll={false}
/>
</Column>
);
}
}
...@@ -2,13 +2,13 @@ import React from 'react'; ...@@ -2,13 +2,13 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandHashtagTimeline } from '../../../actions/timelines'; import { expandHashtagTimeline } from 'mastodon/actions/timelines';
import { connectHashtagStream } from '../../../actions/streaming'; import { connectHashtagStream } from 'mastodon/actions/streaming';
import Masonry from 'react-masonry-infinite'; import Masonry from 'react-masonry-infinite';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import DetailedStatusContainer from '../../status/containers/detailed_status_container'; import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from '../../../components/loading_indicator'; import LoadingIndicator from 'mastodon/components/loading_indicator';
const mapStateToProps = (state, { hashtag }) => ({ const mapStateToProps = (state, { hashtag }) => ({
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()), statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
......
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StatusListContainer from '../../ui/containers/status_list_container'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandPublicTimeline } from '../../../actions/timelines'; import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
import Column from '../../../components/column'; import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
import ColumnHeader from '../../../components/column_header'; import Masonry from 'react-masonry-infinite';
import { defineMessages, injectIntl } from 'react-intl'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { connectPublicStream } from '../../../actions/streaming'; import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container';
import { debounce } from 'lodash';
const messages = defineMessages({ import LoadingIndicator from 'mastodon/components/loading_indicator';
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
}); const mapStateToProps = (state, { local }) => {
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
export default @connect()
@injectIntl return {
statusIds: timeline.get('items', ImmutableList()),
isLoading: timeline.get('isLoading', false),
hasMore: timeline.get('hasMore', false),
};
};
export default @connect(mapStateToProps)
class PublicTimeline extends React.PureComponent { class PublicTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
local: PropTypes.bool,
}; };
handleHeaderClick = () => { componentDidMount () {
this.column.scrollTop(); this._connect();
} }
setRef = c => { componentDidUpdate (prevProps) {
this.column = c; if (prevProps.local !== this.props.local) {
this._disconnect();
this._connect();
}
} }
componentDidMount () { componentWillUnmount () {
const { dispatch } = this.props; this._disconnect();
}
dispatch(expandPublicTimeline()); _connect () {
this.disconnect = dispatch(connectPublicStream()); const { dispatch, local } = this.props;
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
this.disconnect = dispatch(local ? connectCommunityStream() : connectPublicStream());
} }
componentWillUnmount () { _disconnect () {
if (this.disconnect) { if (this.disconnect) {
this.disconnect(); this.disconnect();
this.disconnect = null; this.disconnect = null;
...@@ -44,27 +61,48 @@ class PublicTimeline extends React.PureComponent { ...@@ -44,27 +61,48 @@ class PublicTimeline extends React.PureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
this.props.dispatch(expandPublicTimeline({ maxId })); const { dispatch, local } = this.props;
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
}
setRef = c => {
this.masonry = c;
} }
handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}
this.masonry.forcePack();
}, 50)
render () { render () {
const { intl } = this.props; const { statusIds, hasMore, isLoading } = this.props;
const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
return ( return (
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}> <Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
<ColumnHeader {statusIds.map(statusId => (
icon='globe' <div className='statuses-grid__item' key={statusId}>
title={intl.formatMessage(messages.title)} <DetailedStatusContainer
onClick={this.handleHeaderClick} id={statusId}
/> compact
measureHeight
<StatusListContainer onHeightChange={this.handleHeightChange}
timelineId='public' />
onLoadMore={this.handleLoadMore} </div>
scrollKey='standalone_public_timeline' )).toArray()}
trackScroll={false} </Masonry>
/>
</Column>
); );
} }
......
...@@ -23,7 +23,7 @@ export default class DetailedStatus extends ImmutablePureComponent { ...@@ -23,7 +23,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired, onToggleHidden: PropTypes.func.isRequired,
......
...@@ -193,6 +193,7 @@ $small-breakpoint: 960px; ...@@ -193,6 +193,7 @@ $small-breakpoint: 960px;
} }
strong { strong {
font-family: $font-display, sans-serif;
font-weight: 500; font-weight: 500;
font-size: 32px; font-size: 32px;
line-height: 48px; line-height: 48px;
...@@ -280,168 +281,6 @@ $small-breakpoint: 960px; ...@@ -280,168 +281,6 @@ $small-breakpoint: 960px;
} }
.landing-page { .landing-page {
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: 1fr 2fr;
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
display: none;
}
.column-1 {
grid-column: 1;
grid-row: 1;
}
.column-2 {
grid-column: 2;
grid-row: 1;
}
.column-3 {
grid-column: 3;
grid-row: 1 / 3;
}
.column-4 {
grid-column: 1 / 3;
grid-row: 2;
}
}
@media screen and (max-width: $small-breakpoint) {
.grid {
grid-template-columns: 40% 60%;
.column-0 {
display: none;
}
.column-1 {
grid-column: 1;
grid-row: 1;
&.non-preview .landing-page__forms {
height: 100%;
}
}
.column-2 {
grid-column: 2;
grid-row: 1 / 3;
&.non-preview {
grid-column: 2;
grid-row: 1;
}
}
.column-3 {
grid-column: 1;
grid-row: 2 / 4;
}
.column-4 {
grid-column: 2;
grid-row: 3;
&.non-preview {
grid-column: 1 / 3;
grid-row: 2;
}
}
}
}
@media screen and (max-width: $column-breakpoint) {
.grid {
grid-template-columns: 100%;
.column-0 {
display: block;
grid-column: 1;
grid-row: 1;
}
.column-1 {
grid-column: 1;
grid-row: 3;
.brand {
display: none;
}
}
.column-2 {
grid-column: 1;
grid-row: 2;
.landing-page__logo,
.landing-page__call-to-action {
display: none;
}
&.non-preview {
grid-column: 1;
grid-row: 2;
}
}
.column-3 {
grid-column: 1;
grid-row: 5;
}
.column-4 {
grid-column: 1;
grid-row: 4;
&.non-preview {
grid-column: 1;
grid-row: 4;
}
}
}
}
.column-flex {
display: flex;
flex-direction: column;
}
.separator-or {
position: relative;
margin: 40px 0;
text-align: center;
&::before {
content: "";
display: block;
width: 100%;
height: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
position: absolute;
top: 50%;
left: 0;
}
span {
display: inline-block;
background: $ui-base-color;
font-size: 12px;
font-weight: 500;
color: $darker-text-color;
text-transform: uppercase;
position: relative;
z-index: 1;
padding: 0 8px;
cursor: default;
}
}
p, p,
li { li {
font-family: $font-sans-serif, sans-serif; font-family: $font-sans-serif, sans-serif;
...@@ -458,28 +297,6 @@ $small-breakpoint: 960px; ...@@ -458,28 +297,6 @@ $small-breakpoint: 960px;
} }
} }
.closed-registrations-message {
margin-top: 20px;
&,
p {
text-align: center;
font-size: 12px;
line-height: 18px;
color: $darker-text-color;
margin-bottom: 0;
a {
color: $highlight-text-color;
text-decoration: underline;
}
}
p:last-child {
margin-bottom: 0;
}
}
em { em {
display: inline; display: inline;
margin: 0; margin: 0;
...@@ -593,187 +410,6 @@ $small-breakpoint: 960px; ...@@ -593,187 +410,6 @@ $small-breakpoint: 960px;
} }
} }
.container-alt {
width: 100%;
box-sizing: border-box;
max-width: 800px;
margin: 0 auto;
word-wrap: break-word;
}
.header-wrapper {
padding-top: 15px;
background: $ui-base-color;
background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
position: relative;
&.compact {
background: $ui-base-color;
padding-bottom: 15px;
.hero .heading {
padding-bottom: 20px;
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
font-size: 16px;
line-height: 30px;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
}
}
}
}
.brand {
a {
padding-left: 0;
padding-right: 0;
color: $white;
}
img {
height: 32px;
position: relative;
top: 4px;
left: -10px;
}
}
.header {
line-height: 30px;
overflow: hidden;
.container-alt {
display: flex;
justify-content: space-between;
}
.links {
position: relative;
z-index: 4;
a {
display: flex;
justify-content: center;
align-items: center;
color: $darker-text-color;
text-decoration: none;
padding: 12px 16px;
line-height: 32px;
font-family: $font-display, sans-serif;
font-weight: 500;
font-size: 14px;
&:hover {
color: $secondary-text-color;
}
}
ul {
list-style: none;
margin: 0;
li {
display: inline-block;
vertical-align: bottom;
margin: 0;
&:first-child a {
padding-left: 0;
}
&:last-child a {
padding-right: 0;
}
}
}
}
.hero {
margin-top: 50px;
align-items: center;
position: relative;
.heading {
position: relative;
z-index: 4;
padding-bottom: 150px;
}
.simple_form,
.closed-registrations-message {
background: darken($ui-base-color, 4%);
width: 280px;
padding: 15px 20px;
border-radius: 4px 4px 0 0;
line-height: initial;
position: relative;
z-index: 4;
.actions {
margin-bottom: 0;
button,
.button,
.block-button {
margin-bottom: 0;
}
}
}
.closed-registrations-message {
min-height: 330px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
.about-short {
background: darken($ui-base-color, 4%);
padding: 50px 0 30px;
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
font-size: 16px;
line-height: 30px;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
}
}
&.alternative {
padding: 10px 0;
.brand {
text-align: center;
padding: 30px 0;
margin-bottom: 10px;
img {
position: static;
padding: 10px 0;
}
@media screen and (max-width: $small-breakpoint) {
padding: 15px 0;
}
@media screen and (max-width: $column-breakpoint) {
padding: 0;
margin-bottom: -10px;
}
}
}
&__information, &__information,
&__forms { &__forms {
padding: 20px; padding: 20px;
...@@ -967,353 +603,253 @@ $small-breakpoint: 960px; ...@@ -967,353 +603,253 @@ $small-breakpoint: 960px;
} }
} }
&__forms { @media screen and (max-width: 840px) {
height: 100%; .information-board {
.container-alt {
@media screen and (max-width: $small-breakpoint) { padding-right: 20px;
height: auto;
}
@media screen and (max-width: $column-breakpoint) {
background: transparent;
box-shadow: none;
padding: 0 20px;
margin-top: 30px;
margin-bottom: 40px;
.separator-or {
span {
background: darken($ui-base-color, 8%);
}
} }
}
hr {
margin: 40px 0;
}
.button { .panel {
display: block; position: static;
} margin-top: 20px;
width: 100%;
.subtle-hint a { border-radius: 4px;
text-decoration: none;
&:hover, .panel-header {
&:focus, text-align: center;
&:active { }
text-decoration: underline;
} }
} }
} }
#mastodon-timeline { @media screen and (max-width: 675px) {
display: flex; .header-wrapper {
-webkit-overflow-scrolling: touch; padding-top: 0;
-ms-overflow-style: -ms-autohiding-scrollbar;
font-family: $font-sans-serif, sans-serif;
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $primary-text-color;
width: 100%;
flex: 1 1 auto;
overflow: hidden;
height: 100%;
.column-header {
color: inherit;
font-family: inherit;
font-size: 16px;
line-height: inherit;
font-weight: inherit;
margin: 0;
padding: 0;
}
.column {
padding: 0;
border-radius: 4px;
overflow: hidden;
width: 100%;
}
.scrollable {
height: 400px;
}
p {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
color: $primary-text-color;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
a { &.compact {
color: $secondary-text-color; padding-bottom: 0;
text-decoration: none;
} }
}
.attachment-list__list {
margin-left: 0;
list-style: none;
li {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
margin-bottom: 0;
a {
color: $dark-text-color;
text-decoration: none;
&:hover { &.compact .hero .heading {
text-decoration: underline; text-align: initial;
}
}
} }
} }
@media screen and (max-width: $column-breakpoint) { .header .container-alt,
display: none; .features .container-alt {
display: block;
} }
} }
&__features { .cta {
& > p { margin: 20px;
padding-right: 60px;
}
.features-list {
margin: 40px 0;
margin-top: 30px;
}
&__action {
text-align: center;
}
} }
}
.features-list { .landing {
.features-list__row { margin-bottom: 100px;
display: flex;
padding: 10px 0;
justify-content: space-between;
.visual {
flex: 0 0 auto;
display: flex;
align-items: center;
margin-left: 15px;
.fa { @media screen and (max-width: 738px) {
display: block; margin-bottom: 0;
color: $darker-text-color; }
font-size: 48px;
}
}
.text { &__brand {
font-size: 16px; display: flex;
line-height: 30px; justify-content: center;
color: $darker-text-color; align-items: center;
padding: 100px;
h6 { img {
font-size: inherit; height: 52px;
line-height: inherit;
margin-bottom: 0;
}
}
} }
@media screen and (min-width: $small-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
display: grid; padding: 0;
grid-gap: 30px; margin-bottom: 30px;
grid-template-columns: 1fr 1fr;
grid-auto-columns: 50%;
grid-auto-rows: max-content;
} }
} }
.footer-links { .directory {
padding-bottom: 50px; margin-top: 30px;
text-align: right; background: transparent;
color: $dark-text-color; box-shadow: none;
border-radius: 0;
}
p { .hero-widget {
font-size: 14px; margin-top: 30px;
} margin-bottom: 0;
a { h4 {
color: inherit; padding: 10px;
text-decoration: underline; text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
} }
}
&__footer { &__text {
margin-top: 10px; border-radius: 0;
text-align: center; padding-bottom: 0;
color: $dark-text-color; }
p { &__footer {
font-size: 14px; background: $ui-base-color;
padding: 10px;
border-radius: 0 0 4px 4px;
display: flex;
a { &__column {
color: inherit; flex: 1 1 50%;
text-decoration: underline;
} }
} }
}
@media screen and (max-width: 840px) { .account {
.container-alt { padding: 10px 0;
padding: 0 20px; border-bottom: 0;
}
.information-board { .account__display-name {
.container-alt { display: flex;
padding-right: 20px; align-items: center;
} }
.panel { .account__avatar {
position: static; width: 44px;
margin-top: 20px; height: 44px;
width: 100%; background-size: 44px 44px;
border-radius: 4px;
.panel-header {
text-align: center;
}
} }
} }
}
@media screen and (max-width: 675px) { &__counter {
.header-wrapper { padding: 10px;
padding-top: 0;
&.compact { strong {
padding-bottom: 0; font-family: $font-display, sans-serif;
font-size: 15px;
font-weight: 700;
display: block;
} }
&.compact .hero .heading { span {
text-align: initial; font-size: 14px;
color: $darker-text-color;
} }
} }
}
.header .container-alt, .simple_form .user_agreement .label_input > label {
.features .container-alt { font-weight: 400;
display: block; color: $darker-text-color;
} }
.header {
.links {
padding-top: 15px;
background: darken($ui-base-color, 4%);
a { .simple_form p.lead {
padding: 12px 8px; color: $darker-text-color;
} font-size: 15px;
line-height: 20px;
font-weight: 400;
margin-bottom: 25px;
}
.nav { &__grid {
display: flex; max-width: 960px;
flex-flow: row wrap; margin: 0 auto;
justify-content: space-around; display: grid;
} grid-template-columns: minmax(0, 50%) minmax(0, 50%);
grid-gap: 30px;
.brand img { @media screen and (max-width: 738px) {
left: 0; grid-template-columns: minmax(0, 100%);
top: 0; grid-gap: 10px;
}
}
.hero { &__column-login {
margin-top: 30px; grid-row: 1;
padding: 0; display: flex;
flex-direction: column;
.heading { .box-widget {
padding: 30px 20px; order: 2;
text-align: center; flex: 0 0 auto;
} }
.simple_form, .hero-widget {
.closed-registrations-message { margin-top: 0;
background: darken($ui-base-color, 8%); margin-bottom: 10px;
width: 100%; order: 1;
border-radius: 0; flex: 0 0 auto;
box-sizing: border-box;
} }
} }
}
}
.cta {
margin: 20px;
}
&.tag-page { &__column-registration {
@media screen and (max-width: $column-breakpoint) { grid-row: 2;
padding: 0;
.container {
padding: 0;
} }
#mastodon-timeline { .directory {
display: flex; margin-top: 10px;
height: 100vh;
border-radius: 0;
} }
} }
.grid { @media screen and (max-width: $no-gap-breakpoint) {
@media screen and (min-width: $small-breakpoint) { grid-gap: 0;
grid-template-columns: 33% 67%;
}
.column-2 {
grid-column: 2;
grid-row: 1;
}
}
.brand { .hero-widget {
text-align: unset; display: block;
padding: 0; margin-bottom: 0;
box-shadow: none;
img { &__img,
height: 48px; &__img img,
width: auto; &__footer {
border-radius: 0;
}
} }
}
.cta { .hero-widget,
margin: 0; .box-widget,
.directory__tag {
.button { border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-right: 4px;
} }
}
@media screen and (max-width: $column-breakpoint) { .directory {
.grid { margin-top: 0;
grid-gap: 0;
.column-1 { &__tag {
grid-column: 1; margin-bottom: 0;
grid-row: 1;
}
.column-2 { & > a,
display: none; & > div {
border-radius: 0;
box-shadow: none;
}
&:last-child {
border-bottom: 0;
}
} }
} }
} }
} }
} }
.brand {
position: relative;
text-decoration: none;
}
.brand__tagline {
display: block;
position: absolute;
bottom: -10px;
left: 50px;
width: 300px;
color: $ui-primary-color;
text-decoration: none;
font-size: 14px;
@media screen and (max-width: $no-gap-breakpoint) {
position: static;
width: auto;
margin-top: 20px;
color: $dark-text-color;
}
}
...@@ -68,6 +68,17 @@ code { ...@@ -68,6 +68,17 @@ code {
top: 2px; top: 2px;
left: 0; left: 0;
} }
label a {
color: $highlight-text-color;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
} }
} }
...@@ -305,7 +316,7 @@ code { ...@@ -305,7 +316,7 @@ code {
box-shadow: none; box-shadow: none;
} }
&:focus:invalid { &:focus:invalid:not(:placeholder-shown) {
border-color: lighten($error-red, 12%); border-color: lighten($error-red, 12%);
} }
...@@ -346,6 +357,10 @@ code { ...@@ -346,6 +357,10 @@ code {
} }
} }
.input.disabled {
opacity: 0.5;
}
.actions { .actions {
margin-top: 30px; margin-top: 30px;
display: flex; display: flex;
...@@ -392,6 +407,10 @@ code { ...@@ -392,6 +407,10 @@ code {
background-color: darken($ui-highlight-color, 5%); background-color: darken($ui-highlight-color, 5%);
} }
&:disabled:hover {
background-color: $ui-primary-color;
}
&.negative { &.negative {
background: $error-value-color; background: $error-value-color;
......
...@@ -295,6 +295,11 @@ ...@@ -295,6 +295,11 @@
cursor: default; cursor: default;
} }
&.disabled > div {
opacity: 0.5;
cursor: default;
}
h4 { h4 {
flex: 1 1 auto; flex: 1 1 auto;
font-size: 18px; font-size: 18px;
......
...@@ -21,6 +21,10 @@ class InstancePresenter ...@@ -21,6 +21,10 @@ class InstancePresenter
Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count } Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
end end
def active_user_count
Rails.cache.fetch('active_user_count') { Redis.current.pfcount(*(0..3).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
end
def status_count def status_count
Rails.cache.fetch('local_status_count') { Account.local.joins(:account_stat).sum('account_stats.statuses_count') }.to_i Rails.cache.fetch('local_status_count') { Account.local.joins(:account_stat).sum('account_stats.statuses_count') }.to_i
end end
...@@ -29,6 +33,10 @@ class InstancePresenter ...@@ -29,6 +33,10 @@ class InstancePresenter
Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
end end
def sample_accounts
Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.searchable.joins(:account_stat).popular.limit(3) }
end
def version_number def version_number
Mastodon::Version Mastodon::Version
end end
......
.features-list
.features-list__row
.text
%h6= t 'about.features.real_conversation_title'
= t 'about.features.real_conversation_body'
.visual
= fa_icon 'fw comments'
.features-list__row
.text
%h6= t 'about.features.not_a_product_title'
= t 'about.features.not_a_product_body'
.visual
= fa_icon 'fw users'
.features-list__row
.text
%h6= t 'about.features.within_reach_title'
= t 'about.features.within_reach_body'
.visual
= fa_icon 'fw mobile'
.features-list__row
.text
%h6= t 'about.features.humane_approach_title'
= t 'about.features.humane_approach_body'
.visual
= fa_icon 'fw leaf'
- if @instance_presenter.open_registrations
= render 'registration'
- else
= link_to t('auth.register_elsewhere'), 'https://joinmastodon.org/#getting-started', class: 'button button-primary'
.closed-registrations-message
- if @instance_presenter.closed_registrations_message.blank?
%p= t('about.closed_registrations')
- else
= @instance_presenter.closed_registrations_message.html_safe
.separator-or
%span= t('auth.or')
= link_to t('auth.login'), new_user_session_path, class: 'button button-alternative-2 webapp-btn'
.container-alt.links
.brand
= link_to root_url do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
%ul.nav
%li
- if user_signed_in?
= link_to t('settings.back'), root_url, class: 'webapp-btn'
- else
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
%li= link_to t('about.about_this'), about_more_path
%li
= link_to 'https://joinmastodon.org/#getting-started' do
= "#{t('about.other_instances')}"
%i.fa.fa-external-link{ style: 'padding-left: 5px;' }
= simple_form_for(new_user, url: user_session_path) do |f|
.fields-group
- if use_seamless_external_login?
= f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
- else
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
.actions
= f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
%p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
= simple_form_for(new_user, url: user_registration_path) do |f| = simple_form_for(new_user, url: user_registration_path) do |f|
= f.simple_fields_for :account do |account_fields| %p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))
= account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false .fields-group
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false = f.simple_fields_for :account do |account_fields|
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: !Setting.open_registrations
.actions = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
= f.button :button, t('auth.register'), type: :submit, class: 'button button-primary' = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
.fields-group
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: !Setting.open_registrations
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path) .actions
= f.button :button, Setting.open_registrations ? t('auth.register') : t('auth.registration_closed', instance: site_hostname), type: :submit, class: 'button button-primary', disabled: !Setting.open_registrations
...@@ -3,144 +3,76 @@ ...@@ -3,144 +3,76 @@
- content_for :header_tags do - content_for :header_tags do
%link{ rel: 'canonical', href: about_url }/ %link{ rel: 'canonical', href: about_url }/
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
= javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
= render partial: 'shared/og' = render partial: 'shared/og'
.landing-page.alternative .landing
.container .landing__brand
.grid = link_to root_url, class: 'brand' do
.column-0 = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
.brand %span.brand__tagline=t 'about.tagline'
= link_to root_url do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' .landing__grid
.landing__grid__column.landing__grid__column-registration
- if Setting.timeline_preview .box-widget
.column-1 = render 'registration'
.landing-page__forms
.brand .directory
= link_to root_url do .directory__tag{ class: Setting.profile_directory ? nil : 'disabled' }
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' = optional_link_to Setting.profile_directory, explore_path do
%h4
= render 'forms' = fa_icon 'address-book fw'
= t('about.discover_users')
- else %small= t('about.browse_directory')
.column-1.non-preview
.landing-page__forms .avatar-stack
.brand - @instance_presenter.sample_accounts.each do |account|
= link_to root_url do = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
.directory__tag{ class: Setting.timeline_preview ? nil : 'disabled' }
= render 'forms' = optional_link_to Setting.timeline_preview, public_timeline_path do
%h4
- if Setting.timeline_preview = fa_icon 'globe fw'
.column-2 = t('about.see_whats_happening')
.landing-page__hero %small= t('about.browse_public_posts')
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
.directory__tag
.landing-page__information = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
.landing-page__short-description %h4
.row = fa_icon 'tablet fw'
.landing-page__logo = t('about.get_apps')
= image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' %small= t('about.apps_platforms')
%h1 .landing__grid__column.landing__grid__column-login
= @instance_presenter.site_title .box-widget
%small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) = render 'login'
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) .hero-widget
.hero-widget__img
.landing-page__call-to-action{ dir: 'ltr' } = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
.row
.row__information-board - if @instance_presenter.site_short_description.present?
.information-board__section .hero-widget__text
%span= t 'about.user_count_before'
%strong= number_with_delimiter @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section
%span= t 'about.status_count_before'
%strong= number_with_delimiter @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot
.landing-page__mascot
= image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('elephant_ui_plane.svg'), alt: ''
- else
.column-2.non-preview
.landing-page__hero
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
.landing-page__information
.landing-page__short-description
.row
.landing-page__logo
= image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
%h1
= @instance_presenter.site_title
%small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
.landing-page__call-to-action
.row
.row__information-board
.information-board__section
%span= t 'about.user_count_before'
%strong= number_with_delimiter @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section
%span= t 'about.status_count_before'
%strong= number_with_delimiter @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot
.landing-page__mascot
= image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('elephant_ui_plane.svg'), alt: ''
- if Setting.timeline_preview
.column-3
#mastodon-timeline{ data: { props: Oj.dump(default_props) } }
- if Setting.timeline_preview
.column-4.landing-page__information
.landing-page__features
.features-list
%div
%h3= t 'about.what_is_mastodon'
%p= t 'about.about_mastodon_html'
%div.contact
%h3= t 'about.administered_by'
= account_link_to(@instance_presenter.contact_account, link_to(t('about.learn_more'), about_more_path, class: 'button button-alternative'))
= render 'features'
.landing-page__features__action
= link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
.landing-page__footer
%p %p
= link_to t('about.source_code'), @instance_presenter.source_url = @instance_presenter.site_short_description.html_safe.presence
= " (#{@instance_presenter.version_number})" = link_to about_more_path do
= t('about.learn_more')
- else = fa_icon 'angle-double-right'
.column-4.non-preview.landing-page__information
.landing-page__features .hero-widget__footer
.features-list .hero-widget__footer__column
%div %h4= t 'about.administered_by'
%h3= t 'about.what_is_mastodon'
%p= t 'about.about_mastodon_html' = account_link_to @instance_presenter.contact_account
%div.contact
%h3= t 'about.administered_by' .hero-widget__footer__column
= account_link_to(@instance_presenter.contact_account, link_to(t('about.learn_more'), about_more_path, class: 'button button-alternative')) %h4= t 'about.server_stats'
= render 'features' %div{ style: 'display: flex' }
.hero-widget__counter{ style: 'width: 50%' }
.landing-page__features__action %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
= link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative' %span= t 'about.user_count_after', count: @instance_presenter.user_count
.hero-widget__counter{ style: 'width: 50%' }
.landing-page__footer %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
%p %span
= link_to t('about.source_code'), @instance_presenter.source_url = t 'about.active_count_after'
= " (#{@instance_presenter.version_number})" %abbr{ title: t('about.active_footnote') } *
#modal-container
...@@ -3,23 +3,24 @@ ...@@ -3,23 +3,24 @@
- content_for :content do - content_for :content do
.public-layout .public-layout
.container - unless @hide_navbar
%nav.header .container
.nav-left %nav.header
= link_to root_url, class: 'brand' do .nav-left
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' = link_to root_url, class: 'brand' do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
- if Setting.profile_directory - if Setting.profile_directory
= link_to t('directories.directory'), explore_path, class: 'nav-link optional' = link_to t('directories.directory'), explore_path, class: 'nav-link optional'
= link_to t('about.about_this'), about_more_path, class: 'nav-link optional' = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
.nav-center .nav-center
.nav-right .nav-right
- if user_signed_in? - if user_signed_in?
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn' = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
- else - else
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button' = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
= link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button' = link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button'
.container= yield .container= yield
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter