From 9f887aabae796da5e56cb497648219ebacdd4821 Mon Sep 17 00:00:00 2001 From: therealdarkflamemaster <2392732614@qq.com> Date: Fri, 25 Jun 2021 15:15:58 +0200 Subject: [PATCH] FEATURE: Get notified on corpus changes (#501) && Get notified on changes related to a topic (#152) --- src/components/Header.jsx | 10 +- src/components/Rss.jsx | 47 ++++ src/components/rssPage/Rss.jsx | 337 +++++++++++++++++++++++++++ src/components/rssPage/RssTopic.jsx | 271 +++++++++++++++++++++ src/components/rssPage/Topic.jsx | 112 +++++++++ src/components/rssPage/Viewpoint.jsx | 33 +++ src/index.js | 4 + src/locales/en/messages.po | 28 +++ src/locales/fr/messages.po | 28 +++ src/styles/App.css | 24 ++ 10 files changed, 892 insertions(+), 2 deletions(-) create mode 100644 src/components/Rss.jsx create mode 100644 src/components/rssPage/Rss.jsx create mode 100644 src/components/rssPage/RssTopic.jsx create mode 100644 src/components/rssPage/Topic.jsx create mode 100644 src/components/rssPage/Viewpoint.jsx diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 958e2308..fd421308 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import Authenticated from './Authenticated.jsx'; +import Rss from './Rss.jsx'; class Header extends Component { @@ -13,8 +14,13 @@ class Header extends Component { render() { return (
-
-

{this.state.user}

+
+

+ {this.state.user} +

+
+ +
diff --git a/src/components/Rss.jsx b/src/components/Rss.jsx new file mode 100644 index 00000000..4181a518 --- /dev/null +++ b/src/components/Rss.jsx @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import { Trans } from '@lingui/macro'; +import { Link } from 'react-router-dom'; + +const RssIcon = () => ( + + + + + + + + + + + + + + + + + +); + +class Rss extends Component { + + constructor(props) { + super(props); + this.state = {rss: ''}; + } + + render() { + return ( +
+ +
+ +
+ Flux RSS + +
+ ); + } + +} + +export default Rss; \ No newline at end of file diff --git a/src/components/rssPage/Rss.jsx b/src/components/rssPage/Rss.jsx new file mode 100644 index 00000000..0b01824d --- /dev/null +++ b/src/components/rssPage/Rss.jsx @@ -0,0 +1,337 @@ +import React, { Component } from 'react'; +import by from 'compare-func'; +import queryString from 'query-string'; +import Hypertopic from 'hypertopic'; +import conf from '../../config.js'; +import Viewpoint from '../portfolioPage/Viewpoint.jsx'; +import Corpora from '../portfolioPage/Corpora.jsx'; +import Header from '../Header.jsx'; +import Status from '../portfolioPage/Status.jsx'; +import SearchBar from '../portfolioPage/SearchBar.jsx'; +import { Trans } from '@lingui/macro'; +import { Items } from '../../model.js'; +import { Link } from 'react-router-dom'; + +class RSS extends Component { + constructor() { + super(); + this.state = { + viewpoints: [], + corpora: [], + items: [], + selectedItems: [], + topicsItems: new Map(), + services: [], + corpusSelected: '', + linkRss: '' + }; + this._updateSelection(); + conf.then(x => { + this.setState({services: x.services}); + }); + } + + render() { + let viewpoints = this._getViewpoints(); + let corpora = this._getCorpora(); + let [linkRss, multiCorpus] = this._getRssLinks(); + let tempLinkRss = linkRss.length > 1 ? linkRss[0] : linkRss; + let attributes = new Items(this.state.items) + .getAttributes() + .map(([key, value]) => key.concat(' : ', value)) + .map(x => ({[x]: {name: x}})); + let candidates = this.state.viewpoints.concat(attributes); + return ( +
+
+
+
+
+
+

Flux RSS

+
+
+ +
+
+ + + + +
+
+
+
+ Qu'est-ce qu'un flux RSS ? +
+
+ Les flux RSS sont des fils de contenus . Chaque flux correspond à une rubrique ou une sous-rubrique du site. En vous abonnant à un flux, vous recevrez automatiquement le titre, auteur, et informations importantes des derniers items ajoutés ou modifiés. +
+
+
+
+ Comment utiliser les flux RSS ? +
+
+ Pour pouvoir utiliser les flux RSS, plusieurs solutions existent. Vous pouvez utiliser un agrégateur de flux, via un site web ou une application mobile, comme FlipBoard, Feedly, Netvibes, In oreader ou encore News Blur. Alternativament, un lecteur RSS peut être installé directement sur votre navigateur web. +
+
+
+
+
+
+
+ ); + } + + hasChanged = async () => new Hypertopic((await conf).services) + .get({_id: ''}) + .then(x => + x.update_seq !== this.update_seq && (this.update_seq = x.update_seq) + ); + + componentDidMount() { + let start = new Date().getTime(); + this.hasChanged().then(() => { + this._fetchAll().then(() => { + let end = new Date().getTime(); + let elapsedTime = end - start; + console.log('elapsed time ', elapsedTime); + let intervalTime = Math.max(10000, elapsedTime * 5); + console.log('reload every ', intervalTime); + this._timer = setInterval( + async () => { + this.hasChanged().then(x => { + if (x) this._fetchAll(); + }); + }, + intervalTime + ); + }); + }); + } + + componentDidUpdate(prevProps) { + if (this.props !== prevProps) { + this._updateSelection(); + this._updateSelectedItems(); + } + } + + componentWillUnmount() { + clearInterval(this._timer); + } + + _openRssWindows(tempRssLink) { + if (this.state.linkRss !== '') { + window.open(this.state.linkRss); + } else { + window.open(tempRssLink); + } + } + + _setRssLink(value) { + let servicePrincipal = this.state.services[0]; + let origin = window.location.origin; + let linkRss = `${servicePrincipal}/feed/${encodeURI(value)}?app=${origin}`; + this.setState({linkRss}); + } + + _getRssLinks() { + let servicePrincipal = this.state.services[0]; + let corpora = this._getCorpora(); + let ids = corpora.props.ids; + let multiCorpus = ids.map(id => encodeURI(id)); + let origin = window.location.origin; + let linkRss = multiCorpus.map(corpus => `${servicePrincipal}/feed/${corpus}?app=${origin}`); + return [linkRss, ids]; + } + + _getTopic(id) { + for (let v of this.state.viewpoints) { + if (v[id]) return v[id]; + } + return null; + } + + _updateSelection() { + try { + this.selectionJSON = JSON.parse(queryString.parse(window.location.search).t); + } catch (e) { + this.selectionJSON = { + type: 'intersection', + data: [] + }; + } + this.selection = (this.selectionJSON.hasOwnProperty('data')) + ? this.selectionJSON.data.map(s => (s.selection === undefined) + ? [] + : s.selection).flat() + : []; + this.exclusion = (this.selectionJSON.hasOwnProperty('data')) + ? this.selectionJSON.data.map(s => (s.exclusion === undefined) + ? [] + : s.exclusion).flat() + : []; + } + + _getTopicPath(topicId) { + let topic = this._getTopic(topicId); + let path = (topic && topic.broader) ? this._getTopicPath(topic.broader[0].id) : []; + path.push(topicId); + return path; + } + + _getItemTopicsPaths(item) { + return (item.topic || []).map(t => this._getTopicPath(t.id)); + } + + _getRecursiveItemTopics(item) { + return Array.prototype.concat(...this._getItemTopicsPaths(item)); + } + + _getItemAttributes(item) { + return new Items( + [item] + ).getAttributes() + .map(([key, value]) => key.concat(' : ', value)); + } + + _isSelected(item, list) { + let itemHasValue = list.data.map(l => includes(this._getRecursiveItemTopics(item).concat(this._getItemAttributes(item)), (l.selection || []), (l.exclusion || []), (l.type === 'union'))); + if (list.type === 'union') + return itemHasValue.reduce((c1, c2) => c1 || c2, false); + return itemHasValue.reduce((c1, c2) => c1 && c2, true); + } + + _updateSelectedItems() { + let selectedItems; + if (this.selectionJSON.data.length > 0) + selectedItems = this.state.items.filter(e => this._isSelected(e, this.selectionJSON)); + else + selectedItems = this.state.items; + let topicsItems = new Map(); + for (let e of selectedItems) { + for (let t of this._getRecursiveItemTopics(e)) { + push(topicsItems, t, e.id); + } + } + this.setState({selectedItems, topicsItems}); + } + + async _fetchUser(SETTINGS, hypertopic) { + return hypertopic.getView(`/user/${SETTINGS.user}`) + .then(data => { + let user = data[SETTINGS.user] || {}; + user = { + viewpoint: user.viewpoint || [], + corpus: user.corpus || [] + }; + if (!this.state.viewpoints.length && !this.state.corpora.length) { //TODO compare old and new + this.setState({ viewpoints: user.viewpoint, corpora: user.corpus }); + } + return user; + }); + } + + async _fetchViewpoints(hypertopic, user) { + return hypertopic.getView(user.viewpoint.map(x => `/viewpoint/${x.id}`)) + .then(data => { + let viewpoints = []; + for (let v of this.state.viewpoints) { + let viewpoint = data[v.id]; + viewpoint.id = v.id; + viewpoints.push(viewpoint); + } + this.setState({viewpoints}); + return data; + }); + } + + async _fetchItems(hypertopic) { + return hypertopic.getView(this.state.corpora.map(x => `/corpus/${x.id}`)) + .then(data => { + let items = []; + for (let corpus of this.state.corpora) { + for (let itemId in data[corpus.id]) { + if (!['id', 'name', 'user'].includes(itemId)) { + let item = data[corpus.id][itemId]; + if (!item.name || !item.name.length) { + console.log(`/item/${corpus.id}/${itemId} has no name!`); + } else { + item.id = itemId; + item.corpus = corpus.id; + items.push(item); + } + } + } + } + this.setState({items}); + }); + } + + async _fetchAll() { + let SETTINGS = await conf; + let hypertopic = new Hypertopic(SETTINGS.services); + + return this._fetchUser(SETTINGS, hypertopic) + .then(x => Promise.all([this._fetchViewpoints(hypertopic, x), this._fetchItems(hypertopic)])) + .then(() => this._updateSelectedItems()); + } + + _getViewpoints() { + return this.state.viewpoints.sort(by('name')).map((v, i) => +
+ {i > 0 &&
} + +
+ ); + } + + _getCorpora() { + let ids = this.state.corpora.map(c => c.id); + return ( + + ); + } +} +function includes(array1, array2, array3, union) { + let set1 = new Set(array1); + let arrayHasValue = array2.map(e => set1.has(e)); + let arrayDontHaveValue = array3.map(e => set1.has(e)); + if (union) + return arrayHasValue.reduce((c1, c2) => c1 || c2, false) + || ((array3.length > 0) + ? !arrayDontHaveValue.reduce((c1, c2) => c1 || c2, false) + : false + ); + return arrayHasValue.reduce((c1, c2) => c1 && c2, true) + && ((array3.length > 0) + ? !arrayDontHaveValue.reduce((c1, c2) => c1 && c2, true) + : true + ); +} + +function push(map, topicId, itemId) { + let old = map.get(topicId); + if (old) { + map.set(topicId, old.add(itemId)); + } else { + map.set(topicId, new Set([itemId])); + } +} +export default RSS; diff --git a/src/components/rssPage/RssTopic.jsx b/src/components/rssPage/RssTopic.jsx new file mode 100644 index 00000000..2b87563c --- /dev/null +++ b/src/components/rssPage/RssTopic.jsx @@ -0,0 +1,271 @@ +import React, { Component } from 'react'; +import by from 'compare-func'; +import queryString from 'query-string'; +import Hypertopic from 'hypertopic'; +import conf from '../../config.js'; +import Viewpoint from './Viewpoint.jsx'; +import Corpora from '../portfolioPage/Corpora.jsx'; +import Header from '../Header.jsx'; +import Status from '../portfolioPage/Status.jsx'; +import SearchBar from '../portfolioPage/SearchBar.jsx'; +import { Trans } from '@lingui/macro'; +import { Items } from '../../model.js'; + +class RSSTopic extends Component { + constructor(props) { + super(props); + this.state = { + viewpoints: [], + corpora: [], + items: [], + selectedItems: [], + topicsItems: new Map() + }; + this._updateSelection(); + } + + render() { + let viewpoints = this._getViewpoints(); + let corpora = this._getCorpora(); + let attributes = new Items(this.state.items) + .getAttributes() + .map(([key, value]) => key.concat(' : ', value)) + .map(x => ({[x]: {name: x}})); + let candidates = this.state.viewpoints.concat(attributes); + return ( +
+
+
+
+
+
+

Flux RSS

+
+ {viewpoints} +
+
+
+
+
+
+ ); + } + + hasChanged = async () => new Hypertopic((await conf).services) + .get({_id: ''}) + .then(x => + x.update_seq !== this.update_seq && (this.update_seq = x.update_seq) + ); + + componentDidMount() { + let start = new Date().getTime(); + this.hasChanged().then(() => { + this._fetchAll().then(() => { + let end = new Date().getTime(); + let elapsedTime = end - start; + console.log('elapsed time ', elapsedTime); + let intervalTime = Math.max(10000, elapsedTime * 5); + console.log('reload every ', intervalTime); + this._timer = setInterval( + async () => { + this.hasChanged().then(x => { + if (x) this._fetchAll(); + }); + }, + intervalTime + ); + }); + }); + } + + componentDidUpdate(prevProps) { + if (this.props !== prevProps) { + this._updateSelection(); + this._updateSelectedItems(); + } + } + + componentWillUnmount() { + clearInterval(this._timer); + } + + _getTopic(id) { + for (let v of this.state.viewpoints) { + if (v[id]) return v[id]; + } + return null; + } + + _updateSelection() { + try { + this.selectionJSON = JSON.parse(queryString.parse(window.location.search).t); + } catch (e) { + this.selectionJSON = { + type: 'intersection', + data: [] + }; + } + this.selection = (this.selectionJSON.hasOwnProperty('data')) + ? this.selectionJSON.data.map(s => (s.selection === undefined) + ? [] + : s.selection).flat() + : []; + this.exclusion = (this.selectionJSON.hasOwnProperty('data')) + ? this.selectionJSON.data.map(s => (s.exclusion === undefined) + ? [] + : s.exclusion).flat() + : []; + } + + _getTopicPath(topicId) { + let topic = this._getTopic(topicId); + let path = (topic && topic.broader) ? this._getTopicPath(topic.broader[0].id) : []; + path.push(topicId); + return path; + } + + _getItemTopicsPaths(item) { + return (item.topic || []).map(t => this._getTopicPath(t.id)); + } + + _getRecursiveItemTopics(item) { + return Array.prototype.concat(...this._getItemTopicsPaths(item)); + } + + _getItemAttributes(item) { + return new Items( + [item] + ).getAttributes() + .map(([key, value]) => key.concat(' : ', value)); + } + + _isSelected(item, list) { + let itemHasValue = list.data.map(l => includes(this._getRecursiveItemTopics(item).concat(this._getItemAttributes(item)), (l.selection || []), (l.exclusion || []), (l.type === 'union'))); + if (list.type === 'union') + return itemHasValue.reduce((c1, c2) => c1 || c2, false); + return itemHasValue.reduce((c1, c2) => c1 && c2, true); + } + + _updateSelectedItems() { + let selectedItems; + if (this.selectionJSON.data.length > 0) + selectedItems = this.state.items.filter(e => this._isSelected(e, this.selectionJSON)); + else + selectedItems = this.state.items; + let topicsItems = new Map(); + for (let e of selectedItems) { + for (let t of this._getRecursiveItemTopics(e)) { + push(topicsItems, t, e.id); + } + } + this.setState({selectedItems, topicsItems}); + } + + async _fetchUser(SETTINGS, hypertopic) { + return hypertopic.getView(`/user/${SETTINGS.user}`) + .then(data => { + let user = data[SETTINGS.user] || {}; + user = { + viewpoint: user.viewpoint || [], + corpus: user.corpus || [] + }; + if (!this.state.viewpoints.length && !this.state.corpora.length) { //TODO compare old and new + this.setState({ viewpoints: user.viewpoint, corpora: user.corpus }); + } + return user; + }); + } + + async _fetchViewpoints(hypertopic, user) { + return hypertopic.getView(user.viewpoint.map(x => `/viewpoint/${x.id}`)) + .then(data => { + let viewpoints = []; + for (let v of this.state.viewpoints) { + let viewpoint = data[v.id]; + viewpoint.id = v.id; + viewpoints.push(viewpoint); + } + this.setState({viewpoints}); + return data; + }); + } + + async _fetchItems(hypertopic) { + return hypertopic.getView(this.state.corpora.map(x => `/corpus/${x.id}`)) + .then(data => { + let items = []; + for (let corpus of this.state.corpora) { + for (let itemId in data[corpus.id]) { + if (!['id', 'name', 'user'].includes(itemId)) { + let item = data[corpus.id][itemId]; + if (!item.name || !item.name.length) { + console.log(`/item/${corpus.id}/${itemId} has no name!`); + } else { + item.id = itemId; + item.corpus = corpus.id; + items.push(item); + } + } + } + } + this.setState({items}); + }); + } + + async _fetchAll() { + let SETTINGS = await conf; + let hypertopic = new Hypertopic(SETTINGS.services); + + return this._fetchUser(SETTINGS, hypertopic) + .then(x => Promise.all([this._fetchViewpoints(hypertopic, x), this._fetchItems(hypertopic)])) + .then(() => this._updateSelectedItems()); + } + + _getViewpoints() { + return this.state.viewpoints.sort(by('name')).map((v, i) => +
+ {i > 0 &&
} + +
+ ); + } + + _getCorpora() { + let ids = this.state.corpora.map(c => c.id); + return ( + + ); + } +} +function includes(array1, array2, array3, union) { + let set1 = new Set(array1); + let arrayHasValue = array2.map(e => set1.has(e)); + let arrayDontHaveValue = array3.map(e => set1.has(e)); + if (union) + return arrayHasValue.reduce((c1, c2) => c1 || c2, false) + || ((array3.length > 0) + ? !arrayDontHaveValue.reduce((c1, c2) => c1 || c2, false) + : false + ); + return arrayHasValue.reduce((c1, c2) => c1 && c2, true) + && ((array3.length > 0) + ? !arrayDontHaveValue.reduce((c1, c2) => c1 && c2, true) + : true + ); +} + +function push(map, topicId, itemId) { + let old = map.get(topicId); + if (old) { + map.set(topicId, old.add(itemId)); + } else { + map.set(topicId, new Set([itemId])); + } +} +export default RSSTopic; diff --git a/src/components/rssPage/Topic.jsx b/src/components/rssPage/Topic.jsx new file mode 100644 index 00000000..61e76d3a --- /dev/null +++ b/src/components/rssPage/Topic.jsx @@ -0,0 +1,112 @@ +import React, { Component } from 'react'; +import { withRouter } from 'react-router-dom'; +import by from 'compare-func'; + +class Topic extends Component { + constructor(props) { + super(); + this.handleCollapse = this.handleCollapse.bind(this); + this.handleClick = this.handleClick.bind(this); + let hasSubtopics = (props.topics[props.id].narrower || []).length; + this.state = { + fold: hasSubtopics ? 'Opened' : '' + }; + } + + render() { + let subtopics = this._getSubtopics(); + let isSelected = this.props.selection.includes(this.props.id); + let isExcluded = this.props.exclusion.includes(this.props.id); + let topicClasses = (isSelected ? 'Selected' : '') + ' ' + (isExcluded ? 'Excluded' : ''); + let topic = 'Topic ' + this.state.fold; + + let bullet = getBullet(this.state.fold); + return ( +
  • +
  • + ); + } + + _getSubtopics() { + const topic = this.props.topics[this.props.id]; + return (topic.narrower || []).sort(by('name')).map(t => + + ); + } + + handleCollapse(e) { + e.preventDefault(); + this.setState({fold: fold(this.state.fold)}); + } + + handleClick(e) { + e.preventDefault(); + updateSelectionJSON(this.props.topics, this.props.id, this.props.selectionJSON); + this.props.history.push(this.props.name); + } +} + +function updateSelectionJSON(array, item, selection) { + if (selection === undefined) + return; + let found = selection.data.filter(s => { + let allTopics = [...(s.selection || []), ...(s.exclusion || [])]; + if (allTopics.length === 0 || array[allTopics[0]] === undefined) { + return false; + } + return (!array[allTopics[0]].hasOwnProperty('broader') && !array[item].hasOwnProperty('broader')) + || (array[allTopics[0]].hasOwnProperty('broader') && array[allTopics[0]].broader[0].id) === (array[item].hasOwnProperty('broader') && array[item].broader[0].id); + + }); + + if (found.length === 0) { + selection.data.push({type: 'intersection', selection: [item], exclusion: []}); + } else { + if (!found[0].hasOwnProperty('selection')) + found[0].selection = []; + if (!found[0].hasOwnProperty('exclusion')) + found[0].exclusion = []; + switchPlace(found[0], item); + if ((!Array.isArray(found[0].selection) || !found[0].selection.length) && (!Array.isArray(found[0].exclusion) || !found[0].exclusion.length)) + selection.data.splice(selection.data.indexOf(found[0]), 1); + } +} + +function switchPlace(object, item) { + let index; + if ((index = object.selection.indexOf(item)) > -1) { + object.selection.splice(index, 1); + object.exclusion.push(item); + } else if ((index = object.exclusion.indexOf(item)) > -1) { + object.exclusion.splice(index, 1); + } else { + object.selection.push(item); + } +} + +function fold(x) { + switch (x) { + case 'Closed': return 'Opened'; + case 'Opened': return 'Closed'; + default: return ''; + } +} + +function getBullet(x) { + switch (x) { + case 'Closed': return {className: 'oi oi-caret-right cursor-pointer', title: 'Déplier'}; + case 'Opened': return {className: 'oi oi-caret-bottom cursor-pointer', title: 'Replier'}; + default: return {className: 'oi leaf', title: ''}; + } +} +export default withRouter(Topic); diff --git a/src/components/rssPage/Viewpoint.jsx b/src/components/rssPage/Viewpoint.jsx new file mode 100644 index 00000000..7891f6d6 --- /dev/null +++ b/src/components/rssPage/Viewpoint.jsx @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import by from 'compare-func'; +import Topic from './Topic.jsx'; + +class Viewpoint extends Component { + render() { + let topics = this._getTopics(); + return ( +
    +

    + {this.props.viewpoint.name} +

    +
    +
    +
      + {topics} +
    +
    +
    + ); + } + + _getTopics() { + return (this.props.viewpoint.upper || []).sort(by('name')).map((t) => + + ); + } +} + +export default Viewpoint; diff --git a/src/index.js b/src/index.js index 0d6b1a33..45d8ee20 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import Portfolio from './components/portfolioPage/Portfolio.jsx'; import Item from './components/itemPage/Item.jsx'; import Outliner from './components/viewpointPage/Outliner.jsx'; +import RSS from './components/rssPage/Rss'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'open-iconic/font/css/open-iconic-bootstrap.css'; @@ -14,6 +15,7 @@ import { I18nProvider } from '@lingui/react'; import { setupI18n } from '@lingui/core'; import catalogEn from './locales/en/messages.js'; import catalogFr from './locales/fr/messages.js'; +import RSSTopic from './components/rssPage/RssTopic'; var languages = window.navigator.languages.map(x => x.slice(0, 2)); const catalogList = { @@ -44,6 +46,8 @@ ReactDOM.render( + + diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po index ddcbf032..90cd7820 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -127,3 +127,31 @@ msgstr "Username" #: src/components/itemPage/Attribute.jsx:89 msgid "valeur" msgstr "value" + +#: src/components/Rss.jsx:37 +msgid "Flux RSS" +msgstr "RSS" + +#: src/components/rssPage/Rss.jsx:59 +msgid "S'abonner à un corpus" +msgstr "Subscribe to a corpus" + +#: src/components/rssPage/Rss.jsx:62 +msgid "S'abonner à un catégorie" +msgstr "Subscribe to a topic" + +#: src/components/rssPage/Rss.jsx:68 +msgid "Qu'est-ce qu'un flux RSS ?" +msgstr "What is RSS ?" + +#: src/components/rssPage/Rss.jsx:71 +msgid "Les flux RSS sont des fils de contenus . Chaque flux correspond à une rubrique ou une sous-rubrique du site. En vous abonnant à un flux, vous recevrez automatiquement le titre, auteur, et informations importantes des derniers items ajoutés ou modifiés." +msgstr "RSS is a web feed. Each feed is related to a topic of the website. By subscribing to a feed, you will automatically receive the title, the author and the important information about modified of added items." + +#: src/components/rssPage/Rss.jsx:76 +msgid "Comment utiliser les flux RSS ?" +msgstr "How to use RSS feeds ?" + +#: src/components/rssPage/Rss.jsx:79 +msgid "Pour pouvoir utiliser les flux RSS, plusieurs solutions existent. Vous pouvez utiliser un agrégateur de flux, via un site web ou une application mobile, comme FlipBoard, Feedly, Netvibes, InoReader ou encore News Blur. Alternativament, un lecteur RSS peut être installé directement sur votre navigateur web." +msgstr "There are several ways of using RSS feeds. You can use a RSS feed reader, it can take the form of a website or a mobile application, such as FlipBoard, Feedly, Netvives, InoReader or News Blur. Alternatively, a RSS reader can be directly installed on your web browser." \ No newline at end of file diff --git a/src/locales/fr/messages.po b/src/locales/fr/messages.po index d5f284f6..3efc7ecd 100644 --- a/src/locales/fr/messages.po +++ b/src/locales/fr/messages.po @@ -127,3 +127,31 @@ msgstr "nom d'utilisateur" #: src/components/itemPage/Attribute.jsx:89 msgid "valeur" msgstr "valeur" + +#: src/components/Rss.jsx:37 +msgid "Flux RSS" +msgstr "Flux RSS" + +#: src/components/rssPage/Rss.jsx:59 +msgid "S'abonner à un corpus" +msgstr "S'abonner à un corpus" + +#: src/components/rssPage/Rss.jsx:62 +msgid "S'abonner à un catégorie" +msgstr "S'abonner à un catégorie" + +#: src/components/rssPage/Rss.jsx:68 +msgid "Qu'est-ce qu'un flux RSS ?" +msgstr "Qu'est-ce qu'un flux RSS ?" + +#: src/components/rssPage/Rss.jsx:71 +msgid "Les flux RSS sont des fils de contenus . Chaque flux correspond à une rubrique ou une sous-rubrique du site. En vous abonnant à un flux, vous recevrez automatiquement le titre, auteur, et informations importantes des derniers items ajoutés ou modifiés." +msgstr "Les flux RSS sont des fils de contenus . Chaque flux correspond à une rubrique ou une sous-rubrique du site. En vous abonnant à un flux, vous recevrez automatiquement le titre, auteur, et informations importantes des derniers items ajoutés ou modifiés." + +#: src/components/rssPage/Rss.jsx:76 +msgid "Comment utiliser les flux RSS ?" +msgstr "Comment utiliser les flux RSS ?" + +#: src/components/rssPage/Rss.jsx:79 +msgid "Pour pouvoir utiliser les flux RSS, plusieurs solutions existent. Vous pouvez utiliser un agrégateur de flux, via un site web ou une application mobile, comme FlipBoard, Feedly, Netvibes, InoReader ou encore News Blur. Alternativament, un lecteur RSS peut être installé directement sur votre navigateur web." +msgstr "Pour pouvoir utiliser les flux RSS, plusieurs solutions existent. Vous pouvez utiliser un agrégateur de flux, via un site web ou une application mobile, comme FlipBoard, Feedly, Netvibes, InoReader ou encore News Blur. Alternativament, un lecteur RSS peut être installé directement sur votre navigateur web." diff --git a/src/styles/App.css b/src/styles/App.css index 0923117b..c55eca8b 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -15,6 +15,25 @@ min-height: 55px; } +.App header .rss-parte { + text-align: center; + vertical-align: center; +} + +.App header .rss-icon { + display: inline-block; + height: 18px; + width: 18px; + min-width: 14px; + min-height: 14px; + margin-right: 10px; +} + + +.ExplicativeText { + text-align: left; +} + h1 a, h1 a:hover { color: ivory; text-decoration: none; @@ -31,6 +50,11 @@ h1 a, h1 a:hover { padding: 0 8px; } +.rss-btn a{ + color: lightgrey; + padding: 0 8px; +} + .Authenticated input:not([type='submit']) { border: 0; margin: 1px;