Commit a0f323e9 authored by Eugen Rochko's avatar Eugen Rochko

Instance language filtering, new wizard layout, thumbnails displayed

parent c055b324
......@@ -2,6 +2,7 @@ import React from 'react';
import WizardRow from './WizardRow';
import { Scrollbars } from 'react-custom-scrollbars';
import { FormattedHTMLMessage as FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import WizardLanguageSelectContainer from './WizardLanguageSelectContainer';
const messages = defineMessages({
search: { id: 'wizard.search', defaultMessage: 'Search for an instance' }
......@@ -47,6 +48,10 @@ class Wizard extends React.PureComponent {
<div className='spacer'></div>
<div className='language-select'>
<WizardLanguageSelectContainer />
</div>
<div className='search'>
<label htmlFor='instance-search' className='accessibly-hidden'><FormattedMessage id='wizard.search' defaultMessage='Search for an instance' /></label>
......@@ -67,13 +72,6 @@ class Wizard extends React.PureComponent {
</div>
<div className='wizard'>
<div className='wizard-header'>
<div className='wizard-column'><FormattedMessage id='wizard.column.server' defaultMessage='Server' /></div>
<div className='wizard-column optional-column'><FormattedMessage id='wizard.column.stability' defaultMessage='Stability' /></div>
<div className='wizard-column'><FormattedMessage id='wizard.column.population' defaultMessage='Population' /></div>
<div className='wizard-column optional-column'><FormattedMessage id='wizard.column.theme' defaultMessage='Theme' /></div>
</div>
<Scrollbars className='wizard-content' style={{ height: 500 }} renderThumbVertical={this.renderThumb}>
{instances.map(item =>
<WizardRow key={item.id} instance={item} />
......
......@@ -8,9 +8,10 @@ const getInstances = createSelector(
[
state => state.locale,
state => state.searchValue,
state => state.instancesLocale,
state => state.instances,
],
(_, searchValue, instances) => {
(_, searchValue, locale, instances) => {
searchValue = searchValue.toLowerCase();
return instances.filter(item => {
......@@ -18,6 +19,7 @@ const getInstances = createSelector(
const isSearching = searchValue.length > 0;
return eligible &&
(locale === null || (item.info && item.info.languages.indexOf(locale) !== -1)) &&
(!isSearching || fuzzysearch(searchValue, item.searchable));
});
}
......
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
const options = [
{ value: null, label: <FormattedMessage id='wizard.filters.all_languages' defaultMessage='All' /> },
{ value: 'en', label: 'English' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
{ value: 'pl', label: 'Polski' },
{ value: 'pt', label: 'Português' },
{ value: 'ru', label: 'Русский' },
{ value: 'ja', label: '日本語' },
{ value: 'zh', label: '简体中文' }
];
export default class WizardLanguageSelect extends PureComponent {
state = {
opened: false
};
renderMenu () {
const { value } = this.props;
return (
<div className='dropdown__menu'>
{options.map(option => (
<div key={option.value} className={classNames('dropdown__option', { active: option.value === value })} onClick={this.handleOptionClick.bind(this, option.value)}>
{option.label}
</div>
))}
</div>
);
}
handleOptionClick = (value) => {
this.props.onChange(value);
this.setState({ opened: false });
}
handleClick = () => {
this.setState({ opened: !this.state.opened });
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.setState({ opened: false });
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, false);
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, false);
}
setRef = c => {
this.node = c;
}
render () {
const { opened } = this.state;
return (
<div className={classNames('dropdown', { active: opened })} ref={this.setRef}>
<div className='dropdown__control' tabIndex='0' role='button' onClick={this.handleClick}>A <i className='dropdown__arrow ion-ios-arrow-down' /></div>
{opened && this.renderMenu()}
</div>
);
}
}
import WizardLanguageSelect from './WizardLanguageSelect';
import { connect } from 'react-redux';
import { changeInstancesLocale } from './actions';
const mapStateToProps = state => ({
value: state.instancesLocale
});
const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeInstancesLocale(value))
});
export default connect(mapStateToProps, mapDispatchToProps)(WizardLanguageSelect);
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
const messages = defineMessages({
stable: { id: 'wizard_row.stability.stable', defaultMessage: 'Stable' },
intermittent: { id: 'wizard_row.stability.intermittent', defaultMessage: 'Intermittent' },
awful: { id: 'wizard_row.stability.awful', defaultMessage: 'Awful' },
full: { id: 'wizard_row.population.full', defaultMessage: 'Full' },
medium: { id: 'wizard_row.population.medium', defaultMessage: 'Medium' },
new: { id: 'wizard_row.population.new', defaultMessage: 'New' }
});
const WizardRow = ({ instance, intl }) => {
const theme = (instance.info && instance.info.topic) || 'General';
const theme = (instance.info && instance.info.theme) || 'General';
const description = (instance.info && instance.info.short_description) || theme;
const population = instance.users >= 1000 ? `${intl.formatNumber(instance.users / 1000, { maximumFractionDigits: 1 })}k` : instance.users;
let stabilityColor, stabilityLabel,
populationColor, populationLabel;
populationColor;
if (instance.uptime > 0.95) {
stabilityLabel = intl.formatMessage(messages.stable);
......@@ -27,23 +27,29 @@ const WizardRow = ({ instance, intl }) => {
stabilityColor = 'red';
}
if (!instance.open_registrations) {
populationLabel = intl.formatMessage(messages.full);
if (instance.users > 50000) {
populationColor = 'red';
} else if (instance.users > 10000) {
populationLabel = intl.formatMessage(messages.medium);
} else if (instance.users > 5000) {
populationColor = 'yellow';
} else {
populationLabel = intl.formatMessage(messages.new);
populationColor = 'green';
}
return (
<a href={`https://${instance.name}/about`} target='_blank' rel='noopener' className='wizard-row'>
<div className='wizard-column'>{instance.name}</div>
<div className='wizard-column optional-column'><span className={`indicator-text ${stabilityColor}`} title={`${intl.formatNumber(instance.uptime * 100)}%`}><i className={`indicator ${stabilityColor}`} /> {stabilityLabel}</span></div>
<div className='wizard-column'><span className={`indicator-text ${populationColor}`} title={intl.formatNumber(instance.users)}><i className={`indicator ${populationColor}`} /> {populationLabel}</span></div>
<div className='wizard-column optional-column'>{theme}</div>
<a href={`https://${instance.name}/about`} target='_blank' rel='noopener' className={classNames('wizard-row', { offline: !instance.up })}>
<div className='wizard-row__thumbnail'>
<div style={{ backgroundImage: `url(${instance.thumbnail})` }} />
</div>
<div className='wizard-row__details'>
<div className='wizard-row__name'>{instance.name}</div>
<div className='wizard-row__description'>{description}</div>
</div>
<div className='wizard-row__meta'>
<div className='wizard-row__stability'><span className={`indicator-text ${stabilityColor}`}><i className={`indicator ${stabilityColor}`} /> {stabilityLabel}</span></div>
<div className='wizard-row__population'><span className={`indicator-text ${populationColor}`}><i className={`indicator ${populationColor}`} /> <FormattedMessage id='wizard_row.user_count' defaultMessage='{population} {count, plural, one {person} other {people}}' values={{ population, count: instance.users }} /></span></div>
</div>
</a>
);
};
......
......@@ -5,6 +5,7 @@ const INSTANCES_API_TOKEN = 'JEzPe4Ff5c5WA7k4IP5tx0rJMDzEMFxhmXXZvBG4LFSF0Almf0e
export const INSTANCES_FETCH_SUCCESS = 'INSTANCES_FETCH_SUCCESS';
export const SEARCH_VALUE_CHANGE = 'SEARCH_VALUE_CHANGE';
export const LOCALE_CHANGE = 'LOCALE_CHANGE';
export const INSTANCES_LOCALE_CHANGE = 'INSTANCES_LOCALE_CHANGE';
export function fetchInstances() {
return (dispatch, getState) => {
......@@ -12,7 +13,7 @@ export function fetchInstances() {
return;
}
axios('https://instances.social/api/1.0/instances/list?count=500', {
axios('https://instances.social/api/1.0/instances/list?count=1000', {
headers: {'Authorization': `Bearer ${INSTANCES_API_TOKEN}`},
}).then(response => dispatch(fetchInstancesSuccess(response.data.instances)));
};
......@@ -38,3 +39,10 @@ export function changeLocale(data) {
data,
};
};
export function changeInstancesLocale(data) {
return {
type: INSTANCES_LOCALE_CHANGE,
data,
};
};
@import url('https://fonts.googleapis.com/css?family=Quando|Judson|Montserrat:500|Roboto:400,500,700');
@import url('https://fonts.googleapis.com/css?family=Quando|Judson|Montserrat:400,500|Roboto:400,500,700');
@import url('https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css');
@import 'scss/variables.scss';
......
......@@ -62,10 +62,10 @@
"wizard.text": "Each server is a separate, independently owned gateway into the fediverse. You can talk to your friends regardless of which one you choose, but each will have different moderation policies and interests, so choose the one that feels the most comfortable to you.",
"wizard.tip": "Tip:",
"wizard.tip_text": "This isn’t just your home, it’s also the address that other people can find you by. It’ll be <samp>@username@example.com</samp> like choosing an email address.",
"wizard_row.population.full": "Full",
"wizard_row.population.full": "Large",
"wizard_row.population.medium": "Medium",
"wizard_row.population.new": "New",
"wizard_row.population.new": "Small",
"wizard_row.stability.awful": "Awful",
"wizard_row.stability.intermittent": "Intermittent",
"wizard_row.stability.stable": "Stable"
}
\ No newline at end of file
}
......@@ -2,6 +2,7 @@ import {
INSTANCES_FETCH_SUCCESS,
SEARCH_VALUE_CHANGE,
LOCALE_CHANGE,
INSTANCES_LOCALE_CHANGE,
} from './actions';
const supportedLocales = ['en', 'fr', 'pl', 'es', 'ja', 'de','pt-BR'];
......@@ -20,6 +21,7 @@ const initialState = {
instances: [],
searchValue: '',
locale: initialLocale(),
instancesLocale: null,
};
const createSearchable = item => {
......@@ -37,11 +39,13 @@ const createSearchable = item => {
export default function reducer(state = initialState, action) {
switch(action.type) {
case INSTANCES_FETCH_SUCCESS:
return { ...state, instances: action.data.map(createSearchable) };
return { ...state, instances: action.data.filter(item => !item.dead || !item.open_registrations).map(createSearchable) };
case SEARCH_VALUE_CHANGE:
return { ...state, searchValue: action.data };
case LOCALE_CHANGE:
return { ...state, locale: action.data };
case INSTANCES_LOCALE_CHANGE:
return { ...state, instancesLocale: action.data };
default:
return state;
}
......
......@@ -71,19 +71,72 @@
font-size: 14px;
text-decoration: none;
color: inherit;
justify-content: space-between;
&__details {
width: 75%;
padding: 10px 0;
}
&__meta {
width: 15%;
padding: 10px 0;
}
&__thumbnail {
width: 10%;
padding: 10px 0;
display: flex;
& > div {
margin: 0 10px;
margin-right: 0;
background-color: $darkest;
background-size: cover;
background-position: center;
border-radius: 4px;
width: 100%;
}
}
&:nth-child(even) {
background: lighten($darkest, 2%);
}
&:hover {
color: $lightest;
.wizard-row__name {
color: $lightest;
}
}
&:focus {
background: mix($darkest, $vibrant);
color: $white;
}
&.offline {
color: lighten($darkest, 26%);
.indicator-text {
color: lighten($darkest, 26%) !important;
}
.indicator {
background: lighten($darkest, 26%) !important;
}
&:focus {
color: $white;
.indicator-text {
color: $white !important;
}
.indicator {
background: $white !important;
}
}
}
}
.wizard-column {
......@@ -117,6 +170,19 @@
}
}
.wizard-row__name {
font-weight: 500;
padding: 0 10px;
}
.wizard-row__description {
padding: 0 10px;
color: lighten($darkest, 34%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.indicator-text {
font-weight: 500;
......@@ -172,6 +238,26 @@
.spacer {
flex-grow: 1
}
.language-select {
margin-right: 15px;
.dropdown__control {
background: lighten($darkest, 8%);
transition: none;
border-radius: 4px;
line-height: 36px;
padding: 0 16px;
&:hover {
background-color: lighten($darkest, 12%);
}
}
.dropdown__option {
font-weight: 400;
}
}
}
.search__input {
......@@ -255,6 +341,20 @@
}
}
.wizard-controls {
.language-select {
display: none;
}
}
@media only screen and (min-width: 520px) {
.wizard-controls {
.language-select {
display: block;
}
}
}
@media only screen and (max-width: 800px) {
.wizard-page {
margin: 60px 0;
......@@ -288,6 +388,17 @@
width: 100%;
min-width: 0;
}
.wizard {
.wizard-row__thumbnail,
.wizard-row__meta {
display: none;
}
.wizard-row__details {
width: 100%;
}
}
}
@media only screen and (max-width: 380px) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment