anilukin/frontend-project-11

View on GitHub
src/app.js

Summary

Maintainability
A
0 mins
Test Coverage
import onChange from 'on-change';
import 'bootstrap';
import * as yup from 'yup';
import i18next from 'i18next';
import axios from 'axios';
import resources from './locales/index.js';
import {
  renderForm, renderFeeds, renderPosts, renderModal, initRender,
} from './view.js';

const locale = 'ru';

const getBrushedUrl = (url) => {
  const lastSymb = url.trim().slice(-1);
  if (lastSymb === '/') {
    return url.trim().slice(0, -1);
  }
  return url.trim();
};

const validate = (url, list) => {
  const formSchema = yup.string()
    .url()
    .notOneOf(list);

  return formSchema.validate(url);
};

const parseDataFromRss = (xmlString) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(xmlString, 'text/xml');
  if (!doc.querySelector('rss')) {
    throw new Error('errorUrl.invalidRss');
  }
  const items = [...doc.querySelectorAll('rss>channel>item')].map((item) => {
    const title = item.querySelector('title').textContent;
    const link = item.querySelector('link').textContent;
    const description = item.querySelector('description').textContent;
    return { title, link, description };
  });

  return {
    title: doc.querySelector('rss>channel>title').textContent,
    description: doc.querySelector('rss>channel>description').textContent,
    items,
  };
};

export default () => {
  const i18nInstance = i18next.createInstance();
  i18nInstance.init({
    lng: locale,
    debug: true,
    resources,
  });

  initRender(i18nInstance);

  yup.setLocale({
    string: {
      url: i18nInstance.t('errorUrl.url'),
    },
    mixed: {
      notOneOf: i18nInstance.t('errorUrl.notOneOf'),
    },
  });

  const state = {
    form: {
      state: 'filing',
      errors: [],
    },
    feeds: [],
    posts: [],
    currentPostId: null,
    visitedPostsId: [],
  };

  const delay = 5000;

  const watchedState = onChange(state, (path, value) => {
    if (path === 'form') {
      renderForm(value, i18nInstance);
    }
    if (path === 'feeds') {
      renderFeeds(value, i18nInstance);
    }
    if (path === 'posts') {
      renderPosts(value, state.visitedPostsId, i18nInstance);
    }
    if (path === 'currentPostId') {
      renderModal(value, state.posts, i18nInstance);
    }
  });

  const getDataFromRss = (u) => {
    const url = `https://allorigins.hexlet.app/get?disableCache=true&url=${encodeURIComponent(u)}`;
    return axios.get(url)
      .then((response) => {
        if (response.status !== 200) {
          throw new Error('errorUrl.invalidRss');
        }
        const xmlStr = response.data.contents;
        return parseDataFromRss(xmlStr);
      })
      .catch((ex) => {
        if (ex instanceof axios.AxiosError) {
          throw new Error('errorUrl.network');
        }
        throw ex;
      });
  };

  const formEl = document.querySelector('form');

  let nextFeedId = 0;

  const makeFeed = (d, url) => {
    const feed = {};
    feed.id = nextFeedId;
    nextFeedId += 1;
    feed.title = d.title;
    feed.link = url;
    feed.description = d.description;
    return feed;
  };

  let id = 0;

  const makePosts = (d) => d.items.map((item) => {
    const newItem = { ...item, id, feedId: nextFeedId - 1 };
    id += 1;
    return newItem;
  });

  const updPosts = () => {
    const promise = Promise.all(state.feeds.map(({ link }) => getDataFromRss(link)
      .then((data) => makePosts(data))));
    promise.then((prom) => {
      const existingLinks = watchedState.posts.map(({ link }) => link);
      const newPosts = prom.flat().filter((p) => !existingLinks.includes(p.link));
      watchedState.posts = watchedState.posts.concat(newPosts);
    });
    setTimeout(updPosts, delay);
  };

  formEl.addEventListener('submit', (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const newUrl = getBrushedUrl(formData.get('url'));
    const rssLinks = state.feeds.map((feed) => feed.link);
    validate(newUrl, rssLinks)
      .then(() => {
        getDataFromRss(newUrl).then((data) => {
          watchedState.form = {
            state: 'loadedUrl',
          };
          watchedState.feeds = watchedState.feeds.concat(makeFeed(data, newUrl));
          watchedState.posts = watchedState.posts.concat(makePosts(data));
          e.target.reset();
        })
          .catch((ex) => {
            watchedState.form = {
              errors: [i18nInstance.t(ex.message)],
              state: 'errorUrl',
            };
          });
      })
      .catch((ex) => {
        watchedState.form = {
          errors: ex.errors ?? [i18nInstance.t(ex.message)],
          state: 'errorUrl',
        };
      });
  });

  updPosts();

  const postsContainer = document.querySelector('.posts');
  postsContainer.addEventListener('click', (e) => {
    if (e.target.nodeName === 'BUTTON' || e.target.nodeName === 'A') {
      const postId = e.target.getAttribute('data-id');
      watchedState.currentPostId = postId;
      watchedState.visitedPostsId.push(postId);
    }
  });
};