Skip to content

Commit

Permalink
Merge pull request #4 from MiggRabbid/dev
Browse files Browse the repository at this point in the history
main: edits - 3 (big refactoring)
  • Loading branch information
MiggRabbid authored Dec 21, 2023
2 parents c76ec47 + e9de5c1 commit 458cf5b
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 96 deletions.
103 changes: 48 additions & 55 deletions src/app/app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import onChange from 'on-change';
import uniqueId from 'lodash/uniqueId';
import axios from 'axios';
import urlValidator from './validator';
import parserRss from './parserRss';
import render from './render';
import watcher from './render';

const refreshTiming = 5000;

const errorMessage = (error) => {
switch (error.name) {
case 'ParsingError':
return 'errors.incorrectRss';
case 'AxiosError':
return 'errors.networkError';
default:
return error.message;
}
};

const addProxy = (url) => {
const proxyUrl = new URL('/get', 'https://allorigins.hexlet.app');
proxyUrl.searchParams.append('disableCache', 'true');
Expand All @@ -17,95 +27,80 @@ const addProxy = (url) => {
const getData = (url) => axios.get(addProxy(url), { timeout: 5000 })
.catch((error) => { throw error; });

const getPosts = (feedId, data) => {
const posts = [];
data.items.forEach((item) => posts.unshift({ id: uniqueId(), feedId, ...item }));
return posts;
};

const updateRss = (watchedState) => {
const { feeds, posts } = watchedState.data;
feeds.forEach((feed) => {
const feedPosts = posts.filter((post) => post.feedId === feed.id);
getData(feed.link)
.then((rss) => parserRss(rss))
.then((data) => {
const newPosts = [];
data.items.forEach((item) => {
newPosts.unshift({ id: uniqueId(), feedId: feed.id, ...item });
});
const feedId = feed.id;
const newPosts = getPosts(feedId, data);
const isNewPost = (newPost, oldPosts) => !oldPosts.some((old) => old.link === newPost.link);
const resultPost = newPosts.filter((newPost) => isNewPost(newPost, feedPosts));
watchedState.data.posts.unshift(...resultPost);
});
})
.catch((error) => { console.error(error); });
});
setTimeout(() => updateRss(watchedState), refreshTiming);
};

const processRssData = (watchedState, data, inputUrl) => {
const feedId = uniqueId();
watchedState.data.feeds.push({
id: feedId,
...data.feeds,
link: inputUrl,
});
const currentPosts = [];
data.items.forEach((item) => {
currentPosts.unshift({
id: uniqueId(),
feedId,
...item,
const processRssData = (watchedState, i18next) => {
watchedState.state = 'processing';
const inputUrl = watchedState.currentUrl;
return getData(inputUrl)
.then((rss) => parserRss(rss))
.then((data) => {
const feedId = uniqueId();
watchedState.data.feeds.push({
id: feedId, ...data.feeds, link: inputUrl,
});
const currentPosts = getPosts(feedId, data);
watchedState.data.posts = [...currentPosts, ...watchedState.data.posts];
const message = i18next.t('feedback.uploadedRss');
watchedState.formState.feedbacks = { inputUrl, message };
watchedState.formState.isValid = true;
watchedState.state = 'processed';
});
});
watchedState.data.posts = [...currentPosts, ...watchedState.data.posts];
};

const errorMessage = (error) => {
switch (error.name) {
case 'ParsingError':
return 'errors.incorrectRss';
case 'AxiosError':
return 'errors.networkError';
default:
return error.message;
}
};

export default (state, i18next) => {
const rssForm = document.querySelector('.rss-form');
const input = rssForm.querySelector('input[id="url-input"]');
const button = document.querySelector('button[aria-label="add"]');
const modal = document.querySelector('div[id="modal"]');
const feeds = document.querySelector('.feeds');
const posts = document.querySelector('.posts');

const elements = {
rssForm, button, input, feeds, posts,
rssForm, button, input, modal, feeds, posts,
};

const watchedState = onChange(state, render(elements));

const watchedState = watcher(state, elements);
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(rssForm);
const inputUrl = formData.get('url');
watchedState.currentUrl = inputUrl;
urlValidator(watchedState.data.feeds, inputUrl)
.then(() => {
state.currentUrl = inputUrl;
watchedState.state = 'processing';
return getData(inputUrl);
})
.then((rss) => {
const data = parserRss(rss);
processRssData(watchedState, data, inputUrl);
const message = i18next.t('feedback.uploadedRss');
watchedState.feedbacks.push({ type: 'uploaded', inputUrl, message });
watchedState.state = 'processed';
})
.then(() => processRssData(watchedState, i18next))
.catch((error) => {
console.error(error);
const message = i18next.t(errorMessage(error));
watchedState.feedbacks.push({ type: 'error', inputUrl, message });
watchedState.formState.errors.push({ inputUrl, message });
watchedState.formState.isValid = false;
watchedState.state = 'failed';
console.error(error);
})
.finally(() => {
state.formState.isValid = null;
state.formState.feedbacks = null;
watchedState.state = 'filling';
});
};

const handlePostClick = (event) => {
const element = event.target;
const curId = event.target.dataset.id;
Expand All @@ -115,9 +110,7 @@ export default (state, i18next) => {
}
watchedState.uiState.viewedPostsId.add(curId);
};

rssForm.addEventListener('submit', handleSubmit);
posts.addEventListener('click', handlePostClick);

updateRss(watchedState);
};
3 changes: 0 additions & 3 deletions src/app/parserRss.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@ const extractDataFromItem = (item) => ({

export default (rss) => {
const xml = parser.parseFromString(rss.data.contents, 'application/xml');

const parseError = xml.querySelector('parsererror');
if (parseError) {
const error = new Error(parseError.textContent);
error.name = 'ParsingError';
throw error;
}

const items = xml.querySelectorAll('item');
const data = {
feeds: extractDataFromItem(xml),
items: Array.from(items).map((item) => extractDataFromItem(item)),
};

return data;
};
51 changes: 18 additions & 33 deletions src/app/render.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import onChange from 'on-change';

const renderState = (input, button, value) => {
if (value === 'filling') {
button.disabled = false;
input.disabled = false;
input.value = '';
input.focus();
}
if (value === 'processing') {
const oldFeedback = document.querySelector('.feedback');
Expand All @@ -14,35 +17,32 @@ const renderState = (input, button, value) => {
}
if (value === 'processed') {
input.classList.add('is-valid');
input.focus();
}
if (value === 'failed') {
input.classList.add('is-invalid');
}
};

const renderFeedback = (rssForm, value) => {
const renderFeedback = (state, rssForm, value) => {
const oldFeedback = rssForm.parentElement.querySelector('.feedback');
if (oldFeedback) oldFeedback.remove();

const newFeedback = document.createElement('p');
newFeedback.classList.add('feedback', 'm-0', 'position-absolute', 'small');

if (value.at(-1).type === 'uploaded') {
let newTextContent;
if (value === true) {
newTextContent = state.formState.feedbacks.message;
newFeedback.classList.add('text-success');
}
if (value.at(-1).type === 'error') {
if (value === false) {
newTextContent = state.formState.errors.at(-1).message;
newFeedback.classList.add('text-danger');
}

const newTextContent = value.at(-1).message;
newFeedback.textContent = newTextContent;
rssForm.parentElement.append(newFeedback);
};

const renderFeeds = (feeds, value) => {
let card;

if (feeds.querySelector('.card')) {
card = feeds.querySelector('.card');
} else {
Expand All @@ -52,22 +52,17 @@ const renderFeeds = (feeds, value) => {
<ul class="list-group border-0 rounded-0"></ul>`;
feeds.append(card);
}

const listGroup = feeds.querySelector('.list-group');
listGroup.innerHTML = '';

value.forEach((feed) => {
const li = document.createElement('li');
li.classList.add('list-group-item', 'border-0', 'border-end-0');

const h3 = document.createElement('h3');
h3.classList.add('h6', 'm-0');
h3.textContent = feed.title;

const p = document.createElement('p');
p.classList.add('m-0', 'small', 'text-black-50');
p.textContent = feed.description;

li.append(h3);
li.append(p);
listGroup.prepend(li);
Expand All @@ -76,16 +71,13 @@ const renderFeeds = (feeds, value) => {

const renderPosts = (posts, value, prevValue) => {
let newPosts;

if (prevValue === undefined) {
newPosts = value;
} else if (prevValue !== undefined) {
const isObjInPrevValue = (newPost) => prevValue.some((prevPost) => prevPost.id === newPost.id);
newPosts = value.filter((newPost) => !isObjInPrevValue(newPost));
}

let card;

if (posts.querySelector('.card')) {
card = posts.querySelector('.card');
} else {
Expand All @@ -95,29 +87,24 @@ const renderPosts = (posts, value, prevValue) => {
<ul class="list-group border-0 rounded-0"></ul>`;
posts.append(card);
}

const listGroup = posts.querySelector('.list-group');

newPosts.forEach((post) => {
const li = document.createElement('li');
li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-start', 'border-0', 'border-end-0');

const link = document.createElement('a');
link.href = post.link;
link.classList.add('fw-bold');
link.setAttribute('data-id', post.id);
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.textContent = post.title;

const button = document.createElement('button');
button.type = 'button';
button.classList.add('btn', 'btn-outline-primary', 'btn-sm');
button.setAttribute('data-id', post.id);
button.setAttribute('data-bs-toggle', 'modal');
button.setAttribute('data-bs-target', '#modal');
button.textContent = 'Просмотр';

li.append(link);
li.append(button);
listGroup.prepend(li);
Expand All @@ -126,35 +113,31 @@ const renderPosts = (posts, value, prevValue) => {

const renderViewedLink = (value, prevValue) => {
const findNewId = () => Array.from(new Set([...value].filter((item) => !prevValue.has(item))))[0];

const newId = findNewId();
const link = document.querySelector(`a[data-id="${newId}"]`);

link.classList.remove('fw-bold');
link.classList.add('fw-normal', 'link-secondary');
};

const renderModal = (value) => {
const modal = document.querySelector('div[id="modal"]');
const renderModal = (modal, value) => {
const modalTitle = modal.querySelector('.modal-title');
const modalBody = modal.querySelector('.modal-body');
const read = modal.querySelector('.modal-footer').querySelector('a');

modalTitle.textContent = value.title;
modalBody.textContent = value.description;
read.href = value.link;
};

export default (elements) => (path, value, prevValue) => {
const render = (elements, state) => (path, value, prevValue) => {
const {
rssForm, button, input, feeds, posts,
rssForm, button, input, modal, feeds, posts,
} = elements;
switch (path) {
case 'state':
renderState(input, button, value);
break;
case 'feedbacks':
renderFeedback(rssForm, value);
case 'formState.isValid':
renderFeedback(state, rssForm, value);
break;
case 'data.feeds':
renderFeeds(feeds, value);
Expand All @@ -166,9 +149,11 @@ export default (elements) => (path, value, prevValue) => {
renderViewedLink(value, prevValue);
break;
case 'uiState.vievedPost':
renderModal(value);
renderModal(modal, value);
break;
default:
throw new Error(`Unhandled path: ${path}`);
break;
}
};

export default (state, elements) => onChange(state, render(elements, state));
1 change: 0 additions & 1 deletion src/app/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const urlValidator = (feeds, url) => {
.url()
.required()
.notOneOf(feeds.map((feed) => feed.link));

return schema.validate(url)
.then(() => true)
.catch((error) => { throw error; });
Expand Down
10 changes: 6 additions & 4 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export default () => {
vievedPost: null,
viewedPostsId: new Set(),
},
currentUrl: null,
feedbacks: [],
formState: {
currentUrl: null,
isValid: null,
feedbacks: null,
errors: [],
},
};

const defaultLanguage = 'ru';

const i18nextInstance = i18next.createInstance();
i18nextInstance.init({
lng: defaultLanguage,
Expand Down

0 comments on commit 458cf5b

Please sign in to comment.