Merge remote-tracking branch 'origin/develop' into public-layout-improvements
This commit is contained in:
commit
75c4582ce6
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/></svg>
|
|
Before Width: | Height: | Size: 781 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m3 8 7.89 5.26a2 2 0 0 0 2.22 0L21 8M5 19h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2z"/></svg>
|
|
Before Width: | Height: | Size: 284 B |
|
@ -12,7 +12,7 @@
|
||||||
<%= snippets %>
|
<%= snippets %>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-mode-light no-reduce-motion">
|
<body class="theme-mode-light no-reduce-motion">
|
||||||
<div id="soapbox">
|
<div id="soapbox" class="h-full">
|
||||||
<div class="loading-indicator-wrapper">
|
<div class="loading-indicator-wrapper">
|
||||||
<div class="loading-indicator">
|
<div class="loading-indicator">
|
||||||
<div class="loading-indicator__container">
|
<div class="loading-indicator__container">
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { normalizeAccount } from 'soapbox/normalizers';
|
||||||
|
|
||||||
import { render, screen } from '../../jest/test-helpers';
|
import { render, screen } from '../../jest/test-helpers';
|
||||||
import Avatar from '../avatar';
|
import Avatar from '../avatar';
|
||||||
|
|
||||||
describe('<Avatar />', () => {
|
describe('<Avatar />', () => {
|
||||||
const account = fromJS({
|
const account = normalizeAccount({
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
acct: 'alice',
|
acct: 'alice',
|
||||||
display_name: 'Alice',
|
display_name: 'Alice',
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
import StillImage from 'soapbox/components/still_image';
|
|
||||||
|
|
||||||
export default class Avatar extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
size: PropTypes.number,
|
|
||||||
style: PropTypes.object,
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { account, size, className } = this.props;
|
|
||||||
if (!account) return null;
|
|
||||||
|
|
||||||
// : TODO : remove inline and change all avatars to be sized using css
|
|
||||||
const style = !size ? {} : {
|
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StillImage
|
|
||||||
className={classNames('rounded-full', {
|
|
||||||
[className]: typeof className !== 'undefined',
|
|
||||||
})}
|
|
||||||
style={style}
|
|
||||||
src={account.get('avatar')}
|
|
||||||
alt=''
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import StillImage from 'soapbox/components/still_image';
|
||||||
|
|
||||||
|
import type { Account } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IAvatar {
|
||||||
|
account?: Account | null,
|
||||||
|
size?: number,
|
||||||
|
className?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy avatar component.
|
||||||
|
* @see soapbox/components/ui/avatar/avatar.tsx
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
const Avatar: React.FC<IAvatar> = ({ account, size, className }) => {
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
// : TODO : remove inline and change all avatars to be sized using css
|
||||||
|
const style: React.CSSProperties = !size ? {} : {
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StillImage
|
||||||
|
className={classNames('rounded-full overflow-hidden', className)}
|
||||||
|
style={style}
|
||||||
|
src={account.avatar}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Avatar;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/** Fullscreen gradient used as a backdrop to public pages. */
|
||||||
|
const LandingGradient: React.FC = () => (
|
||||||
|
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-slate-700 via-white dark:via-slate-900 to-gradient-end/10 dark:to-slate-900' />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LandingGradient;
|
|
@ -37,8 +37,8 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-purple/20 to-gradient-blue/20 dark:from-slate-900/25 dark:to-slate-900/50': true,
|
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10 dark:from-slate-900/25 dark:to-slate-900/50': true,
|
||||||
'cursor-pointer hover:from-gradient-purple/30 hover:to-gradient-blue/30 dark:hover:from-slate-900/40 dark:hover:to-slate-900/75': typeof onClick !== 'undefined',
|
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-slate-900/40 dark:hover:to-slate-900/75': typeof onClick !== 'undefined',
|
||||||
})}
|
})}
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
>
|
>
|
||||||
|
|
|
@ -123,7 +123,7 @@ const SidebarNavigation = () => {
|
||||||
return (
|
return (
|
||||||
<SidebarNavigationLink
|
<SidebarNavigationLink
|
||||||
to='/messages'
|
to='/messages'
|
||||||
icon={require('icons/mail.svg')}
|
icon={require('@tabler/icons/icons/mail.svg')}
|
||||||
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -158,7 +158,7 @@ const SidebarNavigation = () => {
|
||||||
|
|
||||||
<SidebarNavigationLink
|
<SidebarNavigationLink
|
||||||
to='/settings'
|
to='/settings'
|
||||||
icon={require('icons/cog.svg')}
|
icon={require('@tabler/icons/icons/settings.svg')}
|
||||||
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
|
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
class StillImage extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
alt: PropTypes.string,
|
|
||||||
autoPlayGif: PropTypes.bool.isRequired,
|
|
||||||
className: PropTypes.node,
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
alt: '',
|
|
||||||
className: '',
|
|
||||||
style: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverToPlay() {
|
|
||||||
const { autoPlayGif, src } = this.props;
|
|
||||||
return src && !autoPlayGif && (src.endsWith('.gif') || src.startsWith('blob:'));
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanvasRef = c => {
|
|
||||||
this.canvas = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageRef = i => {
|
|
||||||
this.img = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImageLoad = () => {
|
|
||||||
if (this.hoverToPlay()) {
|
|
||||||
const img = this.img;
|
|
||||||
const canvas = this.canvas;
|
|
||||||
canvas.width = img.naturalWidth;
|
|
||||||
canvas.height = img.naturalHeight;
|
|
||||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { alt, className, src, style } = this.props;
|
|
||||||
const hoverToPlay = this.hoverToPlay();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-testid='still-image-container' className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
|
||||||
<img src={src} alt={alt} ref={this.setImageRef} onLoad={this.handleImageLoad} />
|
|
||||||
{hoverToPlay && <canvas ref={this.setCanvasRef} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
interface IStillImage {
|
||||||
|
/** Image alt text. */
|
||||||
|
alt?: string,
|
||||||
|
/** Extra class names for the outer <div> container. */
|
||||||
|
className?: string,
|
||||||
|
/** URL to the image */
|
||||||
|
src: string,
|
||||||
|
/** Extra CSS styles on the outer <div> element. */
|
||||||
|
style?: React.CSSProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */
|
||||||
|
const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const autoPlayGif = settings.get('autoPlayGif');
|
||||||
|
|
||||||
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
const img = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const hoverToPlay = (
|
||||||
|
src && !autoPlayGif && (src.endsWith('.gif') || src.startsWith('blob:'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleImageLoad = () => {
|
||||||
|
if (hoverToPlay && canvas.current && img.current) {
|
||||||
|
canvas.current.width = img.current.naturalWidth;
|
||||||
|
canvas.current.height = img.current.naturalHeight;
|
||||||
|
canvas.current.getContext('2d')?.drawImage(img.current, 0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid='still-image-container' className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
||||||
|
<img src={src} alt={alt} ref={img} onLoad={handleImageLoad} />
|
||||||
|
{hoverToPlay && <canvas ref={canvas} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StillImage;
|
|
@ -25,7 +25,7 @@ const Avatar = (props: IAvatar) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StillImage
|
<StillImage
|
||||||
className={classNames('rounded-full', className)}
|
className={classNames('rounded-full overflow-hidden', className)}
|
||||||
style={style}
|
style={style}
|
||||||
src={src}
|
src={src}
|
||||||
alt='Avatar'
|
alt='Avatar'
|
||||||
|
|
|
@ -25,7 +25,7 @@ const useButtonStyles = ({
|
||||||
accent: 'border-transparent text-white bg-accent-500 hover:bg-accent-300 focus:ring-pink-500 focus:ring-2 focus:ring-offset-2',
|
accent: 'border-transparent text-white bg-accent-500 hover:bg-accent-300 focus:ring-pink-500 focus:ring-2 focus:ring-offset-2',
|
||||||
danger: 'border-transparent text-danger-700 bg-danger-100 hover:bg-danger-200 focus:ring-danger-500 focus:ring-2 focus:ring-offset-2',
|
danger: 'border-transparent text-danger-700 bg-danger-100 hover:bg-danger-200 focus:ring-danger-500 focus:ring-2 focus:ring-offset-2',
|
||||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||||
link: 'border-transparent text-primary-600 hover:bg-gray-100 hover:text-primary-700',
|
link: 'border-transparent text-primary-600 dark:text-primary-400 hover:bg-gray-100 hover:text-primary-700 dark:hover:bg-slate-900/50',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
|
|
@ -13,7 +13,7 @@ type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' |
|
||||||
const themes = {
|
const themes = {
|
||||||
default: 'text-gray-900 dark:text-gray-100',
|
default: 'text-gray-900 dark:text-gray-100',
|
||||||
danger: 'text-danger-600',
|
danger: 'text-danger-600',
|
||||||
primary: 'text-primary-600',
|
primary: 'text-primary-600 dark:text-primary-400',
|
||||||
muted: 'text-gray-500 dark:text-gray-400',
|
muted: 'text-gray-500 dark:text-gray-400',
|
||||||
subtle: 'text-gray-400 dark:text-gray-500',
|
subtle: 'text-gray-400 dark:text-gray-500',
|
||||||
success: 'text-success-600',
|
success: 'text-success-600',
|
||||||
|
|
|
@ -132,7 +132,7 @@ const SoapboxMount = () => {
|
||||||
|
|
||||||
const waitlisted = account && !account.source.get('approved', true);
|
const waitlisted = account && !account.source.get('approved', true);
|
||||||
|
|
||||||
const bodyClass = classNames('bg-white dark:bg-slate-900 text-base', {
|
const bodyClass = classNames('bg-white dark:bg-slate-900 text-base h-full', {
|
||||||
'no-reduce-motion': !settings.get('reduceMotion'),
|
'no-reduce-motion': !settings.get('reduceMotion'),
|
||||||
'underline-links': settings.get('underlineLinks'),
|
'underline-links': settings.get('underlineLinks'),
|
||||||
'dyslexic': settings.get('dyslexicFont'),
|
'dyslexic': settings.get('dyslexicFont'),
|
||||||
|
@ -162,7 +162,7 @@ const SoapboxMount = () => {
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<html lang={locale} className={classNames({ dark: darkMode })} />
|
<html lang={locale} className={classNames('h-full', { dark: darkMode })} />
|
||||||
<body className={bodyClass} />
|
<body className={bodyClass} />
|
||||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||||
|
@ -21,46 +22,47 @@ const AuthLayout = () => {
|
||||||
const siteTitle = useAppSelector(state => state.instance.title);
|
const siteTitle = useAppSelector(state => state.instance.title);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='h-full'>
|
||||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-slate-700 via-white dark:via-slate-900 to-cyan-50 dark:to-cyan-900' />
|
<LandingGradient />
|
||||||
|
|
||||||
<main className='relative flex flex-col h-screen'>
|
<main className='relative min-h-full sm:flex sm:items-center sm:justify-center py-12'>
|
||||||
<header className='py-10 flex justify-center relative'>
|
<div className='w-full sm:max-w-lg md:max-w-2xl space-y-8'>
|
||||||
<Link to='/' className='cursor-pointer'>
|
<header className='flex justify-center relative'>
|
||||||
{logo ? (
|
<Link to='/' className='cursor-pointer'>
|
||||||
<img src={logo} alt={siteTitle} className='h-7' />
|
{logo ? (
|
||||||
) : (
|
<img src={logo} alt={siteTitle} className='h-7' />
|
||||||
<SvgIcon
|
) : (
|
||||||
className='w-7 h-7 dark:text-white'
|
<SvgIcon
|
||||||
alt={siteTitle}
|
className='w-7 h-7 dark:text-white'
|
||||||
src={require('@tabler/icons/icons/home.svg')}
|
alt={siteTitle}
|
||||||
/>
|
src={require('@tabler/icons/icons/home.svg')}
|
||||||
)}
|
/>
|
||||||
</Link>
|
)}
|
||||||
</header>
|
</Link>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className='flex flex-col justify-center items-center'>
|
<div className='flex flex-col justify-center items-center'>
|
||||||
<div className='pb-10 sm:mx-auto w-full sm:max-w-lg md:max-w-2xl'>
|
<div className='pb-10 sm:mx-auto w-full sm:max-w-lg md:max-w-2xl'>
|
||||||
<Card variant='rounded' size='xl'>
|
<Card variant='rounded' size='xl'>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path='/verify' component={Verification} />
|
<Route exact path='/verify' component={Verification} />
|
||||||
<Route exact path='/verify/email/:token' component={EmailPassthru} />
|
<Route exact path='/verify/email/:token' component={EmailPassthru} />
|
||||||
<Route exact path='/login/external' component={ExternalLoginForm} />
|
<Route exact path='/login/external' component={ExternalLoginForm} />
|
||||||
<Route exact path='/login' component={LoginPage} />
|
<Route exact path='/login' component={LoginPage} />
|
||||||
<Route exact path='/signup' component={RegistrationForm} />
|
<Route exact path='/signup' component={RegistrationForm} />
|
||||||
<Route exact path='/reset-password' component={PasswordReset} />
|
<Route exact path='/reset-password' component={PasswordReset} />
|
||||||
<Route exact path='/edit-password' component={PasswordResetConfirm} />
|
<Route exact path='/edit-password' component={PasswordResetConfirm} />
|
||||||
<Route path='/invite/:token' component={RegisterInvite} />
|
<Route path='/invite/:token' component={RegisterInvite} />
|
||||||
|
|
||||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||||
<Redirect from='/auth/password/edit' to='/edit-password' />
|
<Redirect from='/auth/password/edit' to='/edit-password' />
|
||||||
</Switch>
|
</Switch>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<BundleContainer fetchComponent={NotificationsContainer}>
|
<BundleContainer fetchComponent={NotificationsContainer}>
|
||||||
|
|
|
@ -21,7 +21,7 @@ const ComposeFormButton: React.FC<IComposeFormButton> = ({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classNames('text-gray-400 hover:text-gray-600', { 'text-primary-600 hover:text-primary-500': active })}
|
className={classNames('text-gray-400 hover:text-gray-600', { 'text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300': active })}
|
||||||
src={icon}
|
src={icon}
|
||||||
title={title}
|
title={title}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -76,7 +76,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
<div className='directory__card__extra'>
|
<div className='directory__card__extra'>
|
||||||
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('statuses_count'))}</Text> <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('statuses_count'))}</Text> <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
||||||
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('followers_count'))}</Text> <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('followers_count'))}</Text> <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
||||||
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <Text theme='primary' size='sm'><FormattedMessage id='account.never_active' defaultMessage='Never' /></Text> : <RelativeTimestamp className='text-primary-600' timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <Text theme='primary' size='sm'><FormattedMessage id='account.never_active' defaultMessage='Never' /></Text> : <RelativeTimestamp className='text-primary-600 dark:text-primary-400' timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -72,7 +72,7 @@ const LandingPage = () => {
|
||||||
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex'>
|
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex'>
|
||||||
<div>
|
<div>
|
||||||
<Stack space={3}>
|
<Stack space={3}>
|
||||||
<h1 className='text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-pink-600 via-primary-500 to-blue-600 sm:mt-5 sm:leading-none lg:mt-6 lg:text-6xl xl:text-7xl'>
|
<h1 className='text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-accent-500 via-primary-500 to-gradient-end sm:mt-5 sm:leading-none lg:mt-6 lg:text-6xl xl:text-7xl'>
|
||||||
{instance.title}
|
{instance.title}
|
||||||
</h1>
|
</h1>
|
||||||
<Text size='lg'>
|
<Text size='lg'>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import Icon from '../../../components/icon';
|
import Icon from '../../../components/icon';
|
||||||
|
@ -204,7 +204,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
src={icons[type]}
|
src={icons[type]}
|
||||||
className='text-primary-600 flex-none'
|
className='text-primary-600 dark:text-primary-400 flex-none'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -286,10 +286,11 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
<HStack alignItems='center' space={1.5}>
|
<HStack alignItems='center' space={1.5}>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
|
|
||||||
<div>
|
<div className='truncate'>
|
||||||
<Text
|
<Text
|
||||||
theme='muted'
|
theme='muted'
|
||||||
size='sm'
|
size='sm'
|
||||||
|
truncate
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useDispatch } from 'react-redux';
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
|
|
||||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||||
|
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||||
import { HStack } from 'soapbox/components/ui';
|
import { HStack } from 'soapbox/components/ui';
|
||||||
|
|
||||||
import AvatarSelectionStep from './steps/avatar-selection-step';
|
import AvatarSelectionStep from './steps/avatar-selection-step';
|
||||||
|
@ -68,7 +69,7 @@ const OnboardingWizard = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='onboarding-wizard'>
|
<div data-testid='onboarding-wizard'>
|
||||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-slate-700 via-white dark:via-slate-900 to-cyan-50 dark:to-cyan-900' />
|
<LandingGradient />
|
||||||
|
|
||||||
<main className='h-screen flex flex-col overflow-x-hidden'>
|
<main className='h-screen flex flex-col overflow-x-hidden'>
|
||||||
<div className='flex flex-col justify-center items-center h-full'>
|
<div className='flex flex-col justify-center items-center h-full'>
|
||||||
|
|
|
@ -73,7 +73,7 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||||
<Card variant='rounded' size='xl'>
|
<Card variant='rounded' size='xl'>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div>
|
<div>
|
||||||
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 border-solid -mx-4 sm:-mx-10'>
|
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-slate-900/50 border-solid -mx-4 sm:-mx-10'>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Text size='2xl' align='center' weight='bold'>
|
<Text size='2xl' align='center' weight='bold'>
|
||||||
<FormattedMessage id='onboarding.avatar.title' defaultMessage='Choose a profile picture' />
|
<FormattedMessage id='onboarding.avatar.title' defaultMessage='Choose a profile picture' />
|
||||||
|
@ -102,7 +102,7 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||||
onClick={openFilePicker}
|
onClick={openFilePicker}
|
||||||
type='button'
|
type='button'
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'absolute bottom-3 right-2 p-1 bg-primary-600 rounded-full ring-2 ring-white hover:bg-primary-700': true,
|
'absolute bottom-3 right-2 p-1 bg-primary-600 rounded-full ring-2 ring-white dark:ring-slate-800 hover:bg-primary-700': true,
|
||||||
'opacity-50 pointer-events-none': isSubmitting,
|
'opacity-50 pointer-events-none': isSubmitting,
|
||||||
})}
|
})}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormattedMessage } from'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ const CompletedStep = ({ onComplete }: { onComplete: () => void }) => (
|
||||||
<Card variant='rounded' size='xl'>
|
<Card variant='rounded' size='xl'>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Icon strokeWidth={1} src={require('@tabler/icons/icons/confetti.svg')} className='w-16 h-16 mx-auto text-primary-600' />
|
<Icon strokeWidth={1} src={require('@tabler/icons/icons/confetti.svg')} className='w-16 h-16 mx-auto text-primary-600 dark:text-primary-400' />
|
||||||
|
|
||||||
<Text size='2xl' align='center' weight='bold'>
|
<Text size='2xl' align='center' weight='bold'>
|
||||||
<FormattedMessage id='onboarding.finished.title' defaultMessage='Onboarding complete' />
|
<FormattedMessage id='onboarding.finished.title' defaultMessage='Onboarding complete' />
|
||||||
|
|
|
@ -8,7 +8,7 @@ const Sonar = () => (
|
||||||
<div className='animate-sonar-scale-2 absolute top-0 left-0 w-full h-full rounded-full bg-primary-600/25 opacity-0 pointer-events-none' />
|
<div className='animate-sonar-scale-2 absolute top-0 left-0 w-full h-full rounded-full bg-primary-600/25 opacity-0 pointer-events-none' />
|
||||||
<div className='animate-sonar-scale-1 absolute top-0 left-0 w-full h-full rounded-full bg-primary-600/25 opacity-0 pointer-events-none' />
|
<div className='animate-sonar-scale-1 absolute top-0 left-0 w-full h-full rounded-full bg-primary-600/25 opacity-0 pointer-events-none' />
|
||||||
|
|
||||||
<div className='absolute top-0 left-0 w-48 h-48 bg-white rounded-full' />
|
<div className='absolute top-0 left-0 w-48 h-48 bg-white dark:bg-slate-900 rounded-full' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
import {
|
import {
|
||||||
NotificationsContainer,
|
NotificationsContainer,
|
||||||
|
@ -35,7 +36,7 @@ class PublicLayout extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-slate-700 via-white dark:via-slate-900 to-cyan-50 dark:to-cyan-900' />
|
<LandingGradient />
|
||||||
|
|
||||||
<div className='flex flex-col h-screen'>
|
<div className='flex flex-col h-screen'>
|
||||||
<div className='flex-shrink-0'>
|
<div className='flex-shrink-0'>
|
||||||
|
|
|
@ -25,8 +25,8 @@ const BackgroundShapes: React.FC<IBackgroundShapes> = ({ position = 'fixed' }) =
|
||||||
<use xlinkHref='#a' />
|
<use xlinkHref='#a' />
|
||||||
</mask>
|
</mask>
|
||||||
<g mask='url(#b)'>
|
<g mask='url(#b)'>
|
||||||
<path className='fill-bg-shape-1' d='M1257.79 335.852C1262 527.117 897.55 530.28 792.32 977.19 600.48 981.41 435.29 545.31 431.08 354.046 426.871 162.782 578.976 4.31 770.815.088c191.844-4.222 482.764 144.5 486.974 335.764Z' fillRule='nonzero' filter='url(#c)' transform='translate(309.54 -367.538)' opacity='.1' />
|
<path className='fill-gradient-end opacity-10 dark:fill-gradient-end dark:opacity-5' d='M1257.79 335.852C1262 527.117 897.55 530.28 792.32 977.19 600.48 981.41 435.29 545.31 431.08 354.046 426.871 162.782 578.976 4.31 770.815.088c191.844-4.222 482.764 144.5 486.974 335.764Z' fillRule='nonzero' filter='url(#c)' transform='translate(309.54 -367.538)' />
|
||||||
<path className='fill-bg-shape-2' d='M71.127 1126.654c206.164 179.412 502.452 211.232 661.777 71.072 159.325-140.163 295.165-510.155 8.23-504.412-320.079 6.405-381.35-817.422-540.675-677.258-31 368-335.497 931.182-129.332 1110.598Z' fillRule='nonzero' filter='url(#d)' transform='translate(309.54 -141.056)' opacity='.1' />
|
<path className='fill-gradient-start opacity-10 dark:fill-gradient-start dark:opacity-5' d='M71.127 1126.654c206.164 179.412 502.452 211.232 661.777 71.072 159.325-140.163 295.165-510.155 8.23-504.412-320.079 6.405-381.35-817.422-540.675-677.258-31 368-335.497 931.182-129.332 1110.598Z' fillRule='nonzero' filter='url(#d)' transform='translate(309.54 -141.056)' />
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -120,7 +120,7 @@ const ReasonStep = (_props: IReasonStep) => {
|
||||||
value={rule.id}
|
value={rule.id}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
readOnly
|
readOnly
|
||||||
className='h-4 w-4 cursor-pointer text-primary-600 border-gray-300 rounded focus:ring-primary-500'
|
className='h-4 w-4 cursor-pointer text-primary-600 dark:text-primary-400 border-gray-300 rounded focus:ring-primary-500'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,7 +31,7 @@ const Success = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={4} alignItems='center'>
|
<Stack space={4} alignItems='center'>
|
||||||
<Icon src={require('@tabler/icons/icons/circle-check.svg')} className='text-primary-600 h-10 w-10' />
|
<Icon src={require('@tabler/icons/icons/circle-check.svg')} className='text-primary-600 dark:text-primary-400 h-10 w-10' />
|
||||||
<Text size='3xl' weight='semibold' align='center'>
|
<Text size='3xl' weight='semibold' align='center'>
|
||||||
{intl.formatMessage(messages.emailConfirmedHeading)}
|
{intl.formatMessage(messages.emailConfirmedHeading)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -34,7 +34,7 @@ const EmailSent = ({ handleSubmit }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='sm:pt-10 mx-auto flex flex-col items-center justify-center'>
|
<div className='sm:pt-10 mx-auto flex flex-col items-center justify-center'>
|
||||||
<Icon src={require('@tabler/icons/icons/send.svg')} className='text-primary-600 h-12 w-12 mb-5' />
|
<Icon src={require('@tabler/icons/icons/send.svg')} className='text-primary-600 dark:text-primary-400 h-12 w-12 mb-5' />
|
||||||
|
|
||||||
<div className='space-y-1 text-center mb-4'>
|
<div className='space-y-1 text-center mb-4'>
|
||||||
<Text weight='bold' size='3xl'>We sent you an email</Text>
|
<Text weight='bold' size='3xl'>We sent you an email</Text>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||||
|
|
||||||
|
@ -23,7 +24,9 @@ const WaitlistPage = ({ account }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-slate-700 via-white dark:via-slate-900 to-cyan-50 dark:to-cyan-900'>
|
<div>
|
||||||
|
<LandingGradient />
|
||||||
|
|
||||||
<main className='relative flex flex-col h-screen max-w-7xl mx-auto px-2 sm:px-6 lg:px-8'>
|
<main className='relative flex flex-col h-screen max-w-7xl mx-auto px-2 sm:px-6 lg:px-8'>
|
||||||
<header className='relative flex justify-between h-16'>
|
<header className='relative flex justify-between h-16'>
|
||||||
<div className='flex-1 flex items-stretch justify-center relative'>
|
<div className='flex-1 flex items-stretch justify-center relative'>
|
||||||
|
|
|
@ -52,8 +52,6 @@ const DEFAULT_COLORS = ImmutableMap<string, any>({
|
||||||
800: '#991b1b',
|
800: '#991b1b',
|
||||||
900: '#7f1d1d',
|
900: '#7f1d1d',
|
||||||
}),
|
}),
|
||||||
'gradient-purple': '#b8a3f9',
|
|
||||||
'gradient-blue': '#9bd5ff',
|
|
||||||
'sea-blue': '#2feecc',
|
'sea-blue': '#2feecc',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -158,8 +156,8 @@ const maybeAddMissingColors = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMa
|
||||||
const colors = soapboxConfig.get('colors');
|
const colors = soapboxConfig.get('colors');
|
||||||
|
|
||||||
const missing = ImmutableMap({
|
const missing = ImmutableMap({
|
||||||
'bg-shape-1': colors.getIn(['accent', '500']),
|
'gradient-start': colors.getIn(['primary', '500']),
|
||||||
'bg-shape-2': colors.getIn(['primary', '500']),
|
'gradient-end': colors.getIn(['accent', '500']),
|
||||||
});
|
});
|
||||||
|
|
||||||
return soapboxConfig.set('colors', missing.mergeDeep(colors));
|
return soapboxConfig.set('colors', missing.mergeDeep(colors));
|
||||||
|
|
|
@ -771,7 +771,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-primary-600 no-underline hover:underline;
|
@apply text-primary-600 dark:text-primary-400 no-underline hover:underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,5 +113,5 @@
|
||||||
.react-datepicker__month-text--keyboard-selected,
|
.react-datepicker__month-text--keyboard-selected,
|
||||||
.react-datepicker__quarter-text--keyboard-selected,
|
.react-datepicker__quarter-text--keyboard-selected,
|
||||||
.react-datepicker__year-text--keyboard-selected {
|
.react-datepicker__year-text--keyboard-selected {
|
||||||
@apply bg-primary-50 hover:bg-primary-100 text-primary-600;
|
@apply bg-primary-50 hover:bg-primary-100 text-primary-600 dark:text-primary-400;
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,7 +193,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.button-secondary {
|
.button.button-secondary {
|
||||||
@apply h-auto py-1.5 px-2.5 text-primary-600 border-primary-600;
|
@apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
|
|
@ -13,5 +13,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention {
|
.mention {
|
||||||
@apply text-primary-600 hover:underline;
|
@apply text-primary-600 dark:text-primary-400 hover:underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,11 +41,9 @@ module.exports = {
|
||||||
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
accent: [300, 500],
|
accent: [300, 500],
|
||||||
'gradient-purple': true,
|
'gradient-start': true,
|
||||||
'gradient-blue': true,
|
'gradient-end': true,
|
||||||
'sea-blue': true,
|
'sea-blue': true,
|
||||||
'bg-shape-1': true,
|
|
||||||
'bg-shape-2': true,
|
|
||||||
}),
|
}),
|
||||||
animation: {
|
animation: {
|
||||||
'sonar-scale-4': 'sonar-scale-4 3s linear infinite',
|
'sonar-scale-4': 'sonar-scale-4 3s linear infinite',
|
||||||
|
|
|
@ -40,14 +40,14 @@ describe('parseColorMatrix()', () => {
|
||||||
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
accent: [300, 500],
|
accent: [300, 500],
|
||||||
'gradient-purple': true,
|
'gradient-start': true,
|
||||||
'gradient-blue': true,
|
'gradient-end': true,
|
||||||
'sea-blue': true,
|
'sea-blue': true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = parseColorMatrix(colorMatrix);
|
const result = parseColorMatrix(colorMatrix);
|
||||||
|
|
||||||
expect(result['sea-blue']({})).toEqual('rgb(var(--color-sea-blue))');
|
expect(result['sea-blue']({})).toEqual('rgb(var(--color-sea-blue))');
|
||||||
expect(result['gradient-purple']({ opacityValue: .7 })).toEqual('rgb(var(--color-gradient-purple) / 0.7)');
|
expect(result['gradient-start']({ opacityValue: .7 })).toEqual('rgb(var(--color-gradient-start) / 0.7)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue