The code powering m.abunchtell.com https://m.abunchtell.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

156 lines
3.6 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import Immutable from 'immutable';
  4. import ImmutablePropTypes from 'react-immutable-proptypes';
  5. import punycode from 'punycode';
  6. import classnames from 'classnames';
  7. const IDNA_PREFIX = 'xn--';
  8. const decodeIDNA = domain => {
  9. return domain
  10. .split('.')
  11. .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
  12. .join('.');
  13. };
  14. const getHostname = url => {
  15. const parser = document.createElement('a');
  16. parser.href = url;
  17. return parser.hostname;
  18. };
  19. export default class Card extends React.PureComponent {
  20. static propTypes = {
  21. card: ImmutablePropTypes.map,
  22. maxDescription: PropTypes.number,
  23. onOpenMedia: PropTypes.func.isRequired,
  24. };
  25. static defaultProps = {
  26. maxDescription: 50,
  27. };
  28. state = {
  29. width: 0,
  30. };
  31. handlePhotoClick = () => {
  32. const { card, onOpenMedia } = this.props;
  33. onOpenMedia(
  34. Immutable.fromJS([
  35. {
  36. type: 'image',
  37. url: card.get('url'),
  38. description: card.get('title'),
  39. meta: {
  40. original: {
  41. width: card.get('width'),
  42. height: card.get('height'),
  43. },
  44. },
  45. },
  46. ]),
  47. 0
  48. );
  49. };
  50. renderLink () {
  51. const { card, maxDescription } = this.props;
  52. let image = '';
  53. let provider = card.get('provider_name');
  54. if (card.get('image')) {
  55. image = (
  56. <div className='status-card__image'>
  57. <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
  58. </div>
  59. );
  60. }
  61. if (provider.length < 1) {
  62. provider = decodeIDNA(getHostname(card.get('url')));
  63. }
  64. const className = classnames('status-card', {
  65. 'horizontal': card.get('width') > card.get('height'),
  66. });
  67. return (
  68. <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
  69. {image}
  70. <div className='status-card__content'>
  71. <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
  72. <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
  73. <span className='status-card__host'>{provider}</span>
  74. </div>
  75. </a>
  76. );
  77. }
  78. renderPhoto () {
  79. const { card } = this.props;
  80. return (
  81. <img
  82. className='status-card-photo'
  83. onClick={this.handlePhotoClick}
  84. role='button'
  85. tabIndex='0'
  86. src={card.get('embed_url')}
  87. alt={card.get('title')}
  88. width={card.get('width')}
  89. height={card.get('height')}
  90. />
  91. );
  92. }
  93. setRef = c => {
  94. if (c) {
  95. this.setState({ width: c.offsetWidth });
  96. }
  97. }
  98. renderVideo () {
  99. const { card } = this.props;
  100. const content = { __html: card.get('html') };
  101. const { width } = this.state;
  102. const ratio = card.get('width') / card.get('height');
  103. const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
  104. return (
  105. <div
  106. ref={this.setRef}
  107. className='status-card-video'
  108. dangerouslySetInnerHTML={content}
  109. style={{ height }}
  110. />
  111. );
  112. }
  113. render () {
  114. const { card } = this.props;
  115. if (card === null) {
  116. return null;
  117. }
  118. switch(card.get('type')) {
  119. case 'link':
  120. return this.renderLink();
  121. case 'photo':
  122. return this.renderPhoto();
  123. case 'video':
  124. return this.renderVideo();
  125. case 'rich':
  126. default:
  127. return null;
  128. }
  129. }
  130. }