Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

main: edits - 3 (big refactoring) #4

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading