├── tsconfig.client.json ├── .npmrc ├── .github ├── FUNDING.yml └── README.md ├── .htmlnanorc ├── .stylelintignore ├── public └── favicon.ico ├── app ├── assets │ ├── favicon.ico │ ├── icon │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 57.png │ │ ├── 60.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 96.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 160.png │ │ ├── 180.png │ │ ├── 192.png │ │ └── 310x150.png │ ├── images │ │ ├── gallica.png │ │ ├── gutenberg.png │ │ ├── wikisource-64.png │ │ ├── inventaire-books.jpg │ │ ├── barcode-scanner-64.png │ │ ├── browser-stack-logo-2.jpg │ │ ├── large │ │ │ └── brittanystevens.jpg │ │ ├── medium │ │ │ └── brittanystevens.jpg │ │ ├── small │ │ │ └── brittanystevens.jpg │ │ ├── inventaire-brittanystevens-13947832357-CC-BY-lighter-blue-4-banner-500px.png │ │ └── wikidata.svg │ ├── fonts │ │ ├── Alegreya-Bold.woff │ │ ├── Gregor-Regular.otf │ │ ├── Montserrat-Bold.woff │ │ ├── Alegreya-Regular.woff │ │ ├── AlegreyaSans-Bold.woff │ │ ├── AlegreyaSans-Regular.woff │ │ ├── Montserrat-Regular.woff │ │ └── forkawesome-webfont.woff │ ├── robots.txt │ ├── actions.jsonld │ └── browserconfig.xml ├── modules │ ├── map │ │ ├── scss │ │ │ └── _map.scss │ │ ├── components │ │ │ ├── lib │ │ │ │ └── map.ts │ │ │ ├── zoom_in_to_display_more.svelte │ │ │ └── group_marker.svelte │ │ └── lib │ │ │ ├── geo.ts │ │ │ ├── leaflet_lite.ts │ │ │ └── navigator_position.ts │ ├── transactions │ │ ├── scss │ │ │ └── transactions_commons.scss │ │ ├── lib │ │ │ ├── cancellable_states.ts │ │ │ ├── info_partials.ts │ │ │ └── transactions_actions.ts │ │ └── components │ │ │ ├── transactions_list.svelte │ │ │ └── transactions_welcome.svelte │ ├── general │ │ ├── components │ │ │ ├── full_screen_loader.svelte │ │ │ ├── image_div.svelte │ │ │ ├── counter.svelte │ │ │ ├── info_tip.svelte │ │ │ ├── images_collage.svelte │ │ │ ├── spinner.svelte │ │ │ └── wrap_toggler.svelte │ │ ├── scss │ │ │ ├── _media_query_thresholds.scss │ │ │ ├── _label.scss │ │ │ ├── _case.scss │ │ │ ├── _shortcut_tip.scss │ │ │ ├── panel_utils.scss │ │ │ ├── _colors.scss │ │ │ ├── _counter.scss │ │ │ ├── _img.scss │ │ │ ├── _links.scss │ │ │ ├── _fonts_and_typo_utils.scss │ │ │ ├── base.scss │ │ │ ├── _flex.scss │ │ │ ├── _background.scss │ │ │ ├── _links_utils.scss │ │ │ ├── _loader.scss │ │ │ ├── _select.scss │ │ │ ├── utils.scss │ │ │ └── _fonts_and_typo.scss │ │ └── lib │ │ │ ├── prevent_form_submit.ts │ │ │ ├── forms.ts │ │ │ ├── confirmation_modal.ts │ │ │ ├── querystring_actions.ts │ │ │ └── feedback.ts │ ├── welcome │ │ ├── scss │ │ │ └── _welcome_layout_commons.scss │ │ └── components │ │ │ └── footer_icon_link.svelte │ ├── entities │ │ ├── scss │ │ │ ├── _entities.scss │ │ │ ├── _relatives_lists.scss │ │ │ ├── _entity_list_compact.scss │ │ │ ├── title_tip.scss │ │ │ └── entity_editors_commons.scss │ │ ├── components │ │ │ ├── editor │ │ │ │ ├── fixed_string_value_display.svelte │ │ │ │ ├── string_value_display.svelte │ │ │ │ ├── lib │ │ │ │ │ └── suggestions │ │ │ │ │ │ ├── property_values_shortlist.ts │ │ │ │ │ │ ├── wdt_P195.ts │ │ │ │ │ │ ├── wdt_P123.ts │ │ │ │ │ │ └── wdt_P31.ts │ │ │ │ ├── image_value_display.svelte │ │ │ │ ├── url_value_display.svelte │ │ │ │ ├── url_value_input.svelte │ │ │ │ ├── display_mode_buttons.svelte │ │ │ │ ├── simple_day_value_input_label.svelte │ │ │ │ └── positive_integer_value_input.svelte │ │ │ ├── language_label.svelte │ │ │ ├── contributions_counts.svelte │ │ │ ├── lib │ │ │ │ ├── edition_layout_helpers.ts │ │ │ │ ├── claim_layout_helpers.ts │ │ │ │ ├── edition_action_helpers.ts │ │ │ │ ├── work_helpers.ts │ │ │ │ └── sort_entities_by.ts │ │ │ ├── patches │ │ │ │ ├── image_preview.svelte │ │ │ │ ├── entity_claim_value.svelte │ │ │ │ └── operation_value_reference.svelte │ │ │ ├── cleanup │ │ │ │ ├── language_selector.svelte │ │ │ │ ├── checkbox.svelte │ │ │ │ └── lib │ │ │ │ │ └── spread_part.ts │ │ │ ├── layouts │ │ │ │ ├── entity_claim_link.svelte │ │ │ │ ├── entity_list_compact_title_row.svelte │ │ │ │ ├── entity_info.svelte │ │ │ │ ├── copiable_text.svelte │ │ │ │ └── claim_value.svelte │ │ │ └── entity_label.svelte │ │ └── lib │ │ │ ├── create_entity.ts │ │ │ ├── entity_refresh.ts │ │ │ ├── get_original_lang.ts │ │ │ ├── editor │ │ │ └── get_langs_data.ts │ │ │ ├── search │ │ │ └── search_by_types.ts │ │ │ └── get_best_lang_value.ts │ ├── inventory │ │ ├── components │ │ │ ├── add │ │ │ │ └── lib │ │ │ │ │ └── embedded_scanner_helpers.ts │ │ │ ├── importer │ │ │ │ ├── external_shelf.svelte │ │ │ │ ├── lib │ │ │ │ │ ├── importers_helpers.ts │ │ │ │ │ ├── candidate_row_helpers.ts │ │ │ │ │ └── import_items_helpers.ts │ │ │ │ ├── import_results.svelte │ │ │ │ ├── candidate_nav.svelte │ │ │ │ └── candidates_section.svelte │ │ │ ├── item_show_standalone.svelte │ │ │ ├── inventory_welcome.svelte │ │ │ ├── item_show_modal.svelte │ │ │ ├── lib │ │ │ │ └── item_creation_helpers.ts │ │ │ ├── shelf_info.svelte │ │ │ ├── item_user_box.svelte │ │ │ ├── item_active_transactions.svelte │ │ │ └── entity_source_logo.svelte │ │ ├── scss │ │ │ └── _shelves_selectors.scss │ │ └── lib │ │ │ ├── importer │ │ │ ├── parsers │ │ │ │ ├── babelio.ts │ │ │ │ ├── decode_html_entities.ts │ │ │ │ └── goodreads.ts │ │ │ └── extract_isbns.ts │ │ │ ├── scanner │ │ │ └── draw_canvas.ts │ │ │ ├── data_validator.ts │ │ │ ├── browser │ │ │ └── get_intersection_work_uris.ts │ │ │ └── add_helpers.ts │ ├── tasks │ │ └── lib │ │ │ └── get_next_task.ts │ ├── user │ │ └── lib │ │ │ ├── oauth.ts │ │ │ ├── password_tests.ts │ │ │ ├── request_logout.ts │ │ │ ├── email_tests.ts │ │ │ ├── username_tests.ts │ │ │ ├── i18n_missing_key.ts │ │ │ └── solve_lang.ts │ ├── groups │ │ ├── scss │ │ │ └── group_settings_commons.scss │ │ ├── lib │ │ │ └── group_actions_alt.ts │ │ └── components │ │ │ ├── group_openness.svelte │ │ │ ├── group_searchability.svelte │ │ │ ├── group_url.svelte │ │ │ ├── group_requests.svelte │ │ │ └── group_members.svelte │ ├── users │ │ ├── invitations.ts │ │ └── components │ │ │ ├── paginated_section_items.svelte │ │ │ ├── lib │ │ │ └── public_users_nav_helpers.ts │ │ │ └── user_infobox.svelte │ ├── settings │ │ ├── lib │ │ │ └── notifications_settings_list.ts │ │ ├── scss │ │ │ └── common_settings.scss │ │ └── components │ │ │ ├── user_position_picker.svelte │ │ │ ├── email_validation.svelte │ │ │ └── notification_toggler.svelte │ ├── search │ │ └── lib │ │ │ ├── find_uri.ts │ │ │ └── search_results_history.ts │ ├── listings │ │ ├── lib │ │ │ ├── stores │ │ │ │ └── user_listings.ts │ │ │ └── entities_typing.ts │ │ └── components │ │ │ ├── listings_layout.svelte │ │ │ └── listing_layout.svelte │ ├── shelves │ │ └── components │ │ │ └── lib │ │ │ └── shelves.ts │ ├── network │ │ └── network.ts │ └── notifications │ │ └── notifications.ts ├── types │ ├── shelf.d.ts │ ├── common.d.ts │ ├── declarations │ │ ├── globals.d.ts │ │ ├── assets.d.ts │ │ ├── svelte.d.ts │ │ └── svelte_context.d.ts │ ├── importer.d.ts │ └── entity.d.ts ├── api │ ├── invitations.ts │ ├── activitypub.ts │ ├── helpers.ts │ ├── transactions.ts │ ├── oauth.ts │ ├── images.ts │ ├── commons.ts │ ├── tasks.ts │ ├── shelves.ts │ ├── data.ts │ ├── auth.ts │ ├── groups.ts │ ├── feeds.ts │ ├── users.ts │ ├── search.ts │ └── endpoint.ts ├── tsconfig.client.json ├── init_polyfills.ts ├── lib │ ├── allow_persistant_query.ts │ ├── components │ │ ├── actions │ │ │ ├── autosize.ts │ │ │ ├── resize_observer.ts │ │ │ ├── viewport.ts │ │ │ └── autofocus.ts │ │ └── stores │ │ │ └── screen.ts │ ├── type_of.ts │ ├── key_events.ts │ ├── reload_once_a_day.ts │ ├── encoding_errors.ts │ ├── location_store.ts │ ├── available_lang_list.ts │ ├── local_storage.ts │ ├── wikimedia │ │ └── commons.ts │ ├── user_content.ts │ ├── icons.ts │ ├── env_config.ts │ ├── regex.ts │ ├── data │ │ └── waiters.ts │ └── isbn.ts ├── initialize.ts ├── config.ts └── init_app_layout.ts ├── .eslintignore ├── scripts ├── githooks │ ├── pre-commit │ ├── post-checkout │ └── pre-merge-commit ├── sitemaps │ ├── config.js │ ├── wrap_urls.js │ ├── generate.js │ ├── write_sitemap.js │ ├── main.xml │ ├── files_commands.js │ └── generate_index.js ├── assets │ ├── weight_history │ │ ├── 2016-03-10-650619f │ │ ├── 2016-03-25-4167e4e │ │ ├── 2016-03-29-1ff5450 │ │ ├── 2016-04-05-42188c3 │ │ ├── 2016-04-24-c0f02b8 │ │ └── 2016-05-14-9cb1cf3 │ └── missing_i18n_strings_ignorelist ├── watch.sh ├── run_unit_tests ├── .eslintrc.cjs ├── build_build_metadata ├── add_log_tips.sh ├── watch_federated.sh ├── lint_fix ├── lint ├── typescript │ └── check_types.sh ├── lint_staged ├── build_and_save_logs.sh ├── check_build_environment.sh └── remove_unused_strings_from_i18n_files ├── tests ├── fixtures │ └── exports │ │ └── babelio │ │ ├── Biblio_export21507.csv │ │ └── Critiques_export1312764.csv ├── utils │ ├── mock_browser_env.ts │ └── utils.ts └── libs │ ├── type_of.ts │ └── uri.ts ├── bundle ├── rules │ ├── images.cjs │ ├── fonts.cjs │ ├── ts.cjs │ ├── postcss.cjs │ ├── css.cjs │ └── scss.cjs ├── terser.cjs ├── plugins │ ├── bundle_analyzer.cjs │ ├── extract_css.cjs │ ├── detect_circular_dependencies.cjs │ ├── dynamic_html_index_plugin.cjs │ └── detect_unused_files.cjs ├── webpack.config.prod.cjs ├── webpack.config.dev.cjs ├── dev_server.cjs └── resolve.cjs ├── config ├── default.cjs └── federated.cjs ├── tsconfig.json ├── .mocharc.cjs ├── svelte.config.cjs ├── .gitignore └── .eslintrc.cli.cjs /tsconfig.client.json: -------------------------------------------------------------------------------- 1 | tsconfig.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: Association_Inventaire 2 | -------------------------------------------------------------------------------- /.htmlnanorc: -------------------------------------------------------------------------------- 1 | { 2 | "collapseWhitespace": false 3 | } 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | **/*.scss 3 | **/*.json 4 | app/assets 5 | app/index.html -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ We moved to Codeberg 👉 https://codeberg.org/inventaire/inventaire-client 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/favicon.ico -------------------------------------------------------------------------------- /app/assets/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/16.png -------------------------------------------------------------------------------- /app/assets/icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/32.png -------------------------------------------------------------------------------- /app/assets/icon/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/57.png -------------------------------------------------------------------------------- /app/assets/icon/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/60.png -------------------------------------------------------------------------------- /app/assets/icon/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/72.png -------------------------------------------------------------------------------- /app/assets/icon/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/76.png -------------------------------------------------------------------------------- /app/assets/icon/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/96.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/dist 2 | node_modules 3 | vendor 4 | scripts/assets 5 | app/assets/js/languages_data.ts 6 | -------------------------------------------------------------------------------- /app/assets/icon/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/114.png -------------------------------------------------------------------------------- /app/assets/icon/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/120.png -------------------------------------------------------------------------------- /app/assets/icon/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/144.png -------------------------------------------------------------------------------- /app/assets/icon/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/152.png -------------------------------------------------------------------------------- /app/assets/icon/160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/160.png -------------------------------------------------------------------------------- /app/assets/icon/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/180.png -------------------------------------------------------------------------------- /app/assets/icon/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/192.png -------------------------------------------------------------------------------- /app/assets/icon/310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/icon/310x150.png -------------------------------------------------------------------------------- /app/assets/images/gallica.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/gallica.png -------------------------------------------------------------------------------- /app/modules/map/scss/_map.scss: -------------------------------------------------------------------------------- 1 | .leaflet-control-attribution{ 2 | @include shy(0.8); 3 | max-height: 1.6em; 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/images/gutenberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/gutenberg.png -------------------------------------------------------------------------------- /app/assets/fonts/Alegreya-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/Alegreya-Bold.woff -------------------------------------------------------------------------------- /app/assets/fonts/Gregor-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/Gregor-Regular.otf -------------------------------------------------------------------------------- /app/assets/fonts/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/Montserrat-Bold.woff -------------------------------------------------------------------------------- /app/assets/images/wikisource-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/wikisource-64.png -------------------------------------------------------------------------------- /app/assets/fonts/Alegreya-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/Alegreya-Regular.woff -------------------------------------------------------------------------------- /app/assets/fonts/AlegreyaSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/AlegreyaSans-Bold.woff -------------------------------------------------------------------------------- /app/assets/images/inventaire-books.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/inventaire-books.jpg -------------------------------------------------------------------------------- /app/types/shelf.d.ts: -------------------------------------------------------------------------------- 1 | import type { ShelfId, Shelf } from '#server/types/shelf' 2 | 3 | export type ShelvesByIds = Record 4 | -------------------------------------------------------------------------------- /app/assets/fonts/AlegreyaSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/AlegreyaSans-Regular.woff -------------------------------------------------------------------------------- /app/assets/fonts/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/Montserrat-Regular.woff -------------------------------------------------------------------------------- /app/assets/fonts/forkawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/fonts/forkawesome-webfont.woff -------------------------------------------------------------------------------- /app/assets/images/barcode-scanner-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/barcode-scanner-64.png -------------------------------------------------------------------------------- /app/assets/images/browser-stack-logo-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/browser-stack-logo-2.jpg -------------------------------------------------------------------------------- /scripts/githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | echo -e "\e[0;30mstarting to lint...\e[0m" 6 | npm run lint-staged 7 | -------------------------------------------------------------------------------- /app/assets/images/large/brittanystevens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/large/brittanystevens.jpg -------------------------------------------------------------------------------- /app/assets/images/medium/brittanystevens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/medium/brittanystevens.jpg -------------------------------------------------------------------------------- /app/assets/images/small/brittanystevens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/small/brittanystevens.jpg -------------------------------------------------------------------------------- /app/modules/transactions/scss/transactions_commons.scss: -------------------------------------------------------------------------------- 1 | @mixin event-icon(){ 2 | padding-block-start: 0.3em; 3 | padding-block-end: 0.4em; 4 | } 5 | -------------------------------------------------------------------------------- /scripts/sitemaps/config.js: -------------------------------------------------------------------------------- 1 | export const folderPath = 'public/sitemaps' 2 | export const main = 'main.xml' 3 | export const index = 'sitemapindex.xml' 4 | -------------------------------------------------------------------------------- /app/modules/general/components/full_screen_loader.svelte: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /app/types/common.d.ts: -------------------------------------------------------------------------------- 1 | // Timeouts are identified by number in the browsers (while NodeJs uses Timeout objects) 2 | export type TimeoutId = ReturnType 3 | -------------------------------------------------------------------------------- /tests/fixtures/exports/babelio/Biblio_export21507.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/tests/fixtures/exports/babelio/Biblio_export21507.csv -------------------------------------------------------------------------------- /bundle/rules/images.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.(png|svg|jpg|gif)$/, 3 | // See https://webpack.js.org/guides/asset-modules/ 4 | type: 'asset/resource', 5 | } 6 | -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-03-10-650619f: -------------------------------------------------------------------------------- 1 | -- date: 2016-03-10 - commit: 650619f -- 2 | 124K app.js.gz 3 | 244K vendor.js.gz 4 | 36K app.css.gz 5 | 12K vendor.css.gz 6 | -------------------------------------------------------------------------------- /bundle/rules/fonts.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.(woff|woff2|eot|ttf|otf)$/, 3 | // See https://webpack.js.org/guides/asset-modules/ 4 | type: 'asset/resource', 5 | } 6 | -------------------------------------------------------------------------------- /config/default.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | inventaireServerHost: 'http://localhost:3006', 3 | webpackDevServer: { 4 | host: '0.0.0.0', 5 | port: 3005, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/exports/babelio/Critiques_export1312764.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/tests/fixtures/exports/babelio/Critiques_export1312764.csv -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-03-25-4167e4e: -------------------------------------------------------------------------------- 1 | -- date: 2016-03-29 - commit: 4167e4e -- 2 | 128K app.js.gz 3 | 276K vendor.js.gz 4 | 36K app.css.gz 5 | 12K vendor.css.gz 6 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export FORCE_COLOR=1 5 | 6 | webpack serve --config ./bundle/webpack.config.dev.cjs | ./scripts/add_log_tips.sh 7 | -------------------------------------------------------------------------------- /scripts/run_unit_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ "$1" != "" ] ; then 4 | mocha --exit $MOCHA_OPTIONS --recursive "$@" 5 | else 6 | mocha --exit $MOCHA_OPTIONS --recursive tests 7 | fi 8 | -------------------------------------------------------------------------------- /app/api/invitations.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointPathBuilders } from './endpoint.ts' 2 | 3 | const { action } = getEndpointPathBuilders('invitations') 4 | 5 | export default { byEmails: action('by-emails') } 6 | -------------------------------------------------------------------------------- /app/modules/general/scss/_media_query_thresholds.scss: -------------------------------------------------------------------------------- 1 | // Media query thresolds 2 | // Keep in sync with client/app/lib/screen 3 | $small-screen: 1000px; 4 | $smaller-screen: 600px; 5 | $very-small-screen: 350px; 6 | -------------------------------------------------------------------------------- /scripts/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.cjs', 3 | plugins: [ 4 | 'node-import', 5 | ], 6 | rules: { 7 | 'node-import/prefer-node-protocol': 2, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /app/api/activitypub.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointPathBuilders } from './endpoint.ts' 2 | 3 | const { action } = getEndpointPathBuilders('activitypub') 4 | 5 | export default { 6 | followers (params) { return action('followers', params) }, 7 | } 8 | -------------------------------------------------------------------------------- /app/modules/welcome/scss/_welcome_layout_commons.scss: -------------------------------------------------------------------------------- 1 | @import '#general/scss/utils'; 2 | 3 | $welcome-bg-filter: rgba(#123456, 0.5); 4 | 5 | h3{ 6 | @include serif; 7 | margin-block-start: 0.5em; 8 | color: $dark-grey; 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/images/inventaire-brittanystevens-13947832357-CC-BY-lighter-blue-4-banner-500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inventaire/inventaire-client/HEAD/app/assets/images/inventaire-brittanystevens-13947832357-CC-BY-lighter-blue-4-banner-500px.png -------------------------------------------------------------------------------- /scripts/assets/missing_i18n_strings_ignorelist: -------------------------------------------------------------------------------- 1 | # computed 2 | change username 3 | invalid email 4 | invalid username 5 | that's already your username 6 | 7 | # matching problem 8 | means "inventory" in French 9 | merge & deduplicate works 10 | -------------------------------------------------------------------------------- /bundle/terser.cjs: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require('terser-webpack-plugin') 2 | 3 | module.exports = new TerserPlugin({ 4 | extractComments: false, 5 | terserOptions: { 6 | output: { 7 | comments: false, 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-03-29-1ff5450: -------------------------------------------------------------------------------- 1 | -- date: 2016-03-29 - commit: 1ff5450 -- 2 | 608K app.js 3 | 128K app.js.gz 4 | 840K vendor.js 5 | 244K vendor.js.gz 6 | 288K app.css 7 | 36K app.css.gz 8 | 40K vendor.css 9 | 12K vendor.css.gz 10 | -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-04-05-42188c3: -------------------------------------------------------------------------------- 1 | -- date: 2016-04-05 - commit: 42188c3 -- 2 | 616K app.js 3 | 128K app.js.gz 4 | 800K vendor.js 5 | 232K vendor.js.gz 6 | 292K app.css 7 | 36K app.css.gz 8 | 40K vendor.css 9 | 12K vendor.css.gz 10 | -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-04-24-c0f02b8: -------------------------------------------------------------------------------- 1 | -- date: 2016-04-24 - commit: c0f02b8 -- 2 | 624K app.js 3 | 132K app.js.gz 4 | 780K vendor.js 5 | 224K vendor.js.gz 6 | 244K app.css 7 | 36K app.css.gz 8 | 40K vendor.css 9 | 12K vendor.css.gz 10 | -------------------------------------------------------------------------------- /scripts/assets/weight_history/2016-05-14-9cb1cf3: -------------------------------------------------------------------------------- 1 | -- date: 2016-05-14 - commit: 9cb1cf3 -- 2 | 576K app.js 3 | 128K app.js.gz 4 | 780K vendor.js 5 | 224K vendor.js.gz 6 | 248K app.css 7 | 36K app.css.gz 8 | 40K vendor.css 9 | 12K vendor.css.gz 10 | -------------------------------------------------------------------------------- /scripts/build_build_metadata: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p ./app/assets/js 3 | i18n_content_hash=$(cat ./public/i18n/*.json | sha256sum | cut -c1-7) 4 | echo "export const i18nContentHash = '${i18n_content_hash}'" > ./app/assets/js/build_metadata.js 5 | -------------------------------------------------------------------------------- /app/api/helpers.ts: -------------------------------------------------------------------------------- 1 | import { config } from '#app/config' 2 | import { i18nContentHash } from '#assets/js/build_metadata' 3 | 4 | export function getBuster () { 5 | if (config.env === 'production') return `?${i18nContentHash}` 6 | else return `?${Date.now()}` 7 | } 8 | -------------------------------------------------------------------------------- /config/federated.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | inventaireServerHost: 'http://localhost:3016', 3 | webpackDevServer: { 4 | host: '0.0.0.0', 5 | // By convention, federated server port = (equivalent default server port + 10) 6 | port: 3015, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /scripts/add_log_tips.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export yellow=$'\e[0;33m' 3 | export color_off=$'\e[0m' 4 | 5 | awk '1;/Found/{printf ENVIRON["yellow"] "Persisting type errors might be fixed by clearing types build cache: rm ./tsconfig.tsbuildinfo" ENVIRON["color_off"]}' -------------------------------------------------------------------------------- /app/modules/entities/scss/_entities.scss: -------------------------------------------------------------------------------- 1 | .uri{ 2 | color: $grey; 3 | font-size: 0.8rem; 4 | } 5 | 6 | .property-value{ 7 | color: $grey; 8 | } 9 | 10 | .wikisource{ 11 | .icon{ 12 | height: 1em; 13 | padding-inline-end: 0.35em; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | }, 6 | "include": [ 7 | "app", 8 | "tests" 9 | ], 10 | "references": [ 11 | { "path": "../tsconfig.build.json" }, 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /entity/new 3 | Disallow: /entity/*/*$ 4 | Disallow: /items 5 | Disallow: /search 6 | Disallow: /tasks 7 | Disallow: /transactions 8 | Crawl-delay: 10 9 | 10 | Sitemap: https://inventaire.io/public/sitemaps/sitemapindex.xml 11 | -------------------------------------------------------------------------------- /app/modules/inventory/components/add/lib/embedded_scanner_helpers.ts: -------------------------------------------------------------------------------- 1 | export function displayAboveTopBar () { 2 | document.getElementById('main').style['z-index'] = '2' 3 | } 4 | 5 | export function resetDisplay () { 6 | document.getElementById('main').style['z-index'] = '0' 7 | } 8 | -------------------------------------------------------------------------------- /app/api/transactions.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointPathBuilders } from './endpoint.ts' 2 | 3 | const { base, action } = getEndpointPathBuilders('transactions') 4 | 5 | export default { 6 | base, 7 | byItem: itemId => { 8 | return action('by-item', { item: itemId }) 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /bundle/plugins/bundle_analyzer.cjs: -------------------------------------------------------------------------------- 1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 2 | 3 | module.exports = new BundleAnalyzerPlugin({ 4 | analyzerMode: 'static', 5 | reportFilename: 'bundle_report.html', 6 | generateStatsFile: false, 7 | openAnalyzer: false, 8 | }) 9 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/fixed_string_value_display.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{value}

6 | 7 | 13 | -------------------------------------------------------------------------------- /app/modules/map/components/lib/map.ts: -------------------------------------------------------------------------------- 1 | import { compact, pluck } from 'underscore' 2 | import { forceArray } from '#app/lib/utils' 3 | 4 | export const getDocsBounds = docsWithPosition => { 5 | docsWithPosition = forceArray(docsWithPosition) 6 | return compact(pluck(docsWithPosition, 'position')) 7 | } 8 | -------------------------------------------------------------------------------- /app/assets/actions.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://schema.org", 3 | "@type": "WebSite", 4 | "url": "https://inventaire.io/", 5 | "potentialAction": { 6 | "@type": "SearchAction", 7 | "target": "https://inventaire.io/search?q={search_term}", 8 | "query-input": "required name=search_term" 9 | } 10 | } -------------------------------------------------------------------------------- /app/tsconfig.client.json: -------------------------------------------------------------------------------- 1 | // Some tools (namely VSCode Eslint plugin) do not look for tsconfig.client.json (declared in .eslintrc.cjs) 2 | // in the root folder but in app. It's either misconfiguration or a bug, but until this can be fixed, 3 | // this file does the trick 4 | { 5 | "extends": "../tsconfig.client.json" 6 | } 7 | -------------------------------------------------------------------------------- /app/modules/entities/scss/_relatives_lists.scss: -------------------------------------------------------------------------------- 1 | @mixin relatives-lists-commons(){ 2 | /*Large screens*/ 3 | @media screen and (width >= $small-screen) { 4 | @include display-flex(row, stretch, flex-start, wrap); 5 | gap: 0.5rem; 6 | :global(.relative-entities-list){ 7 | flex: 1 0 40%; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/watch_federated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | export NODE_ENV=federated 5 | export FORCE_COLOR=1 6 | 7 | custom_ansi_colors(){ 8 | sed 's@\[32m@\[35m@g' 9 | } 10 | 11 | webpack serve --config ./bundle/webpack.config.dev.cjs 2> >(custom_ansi_colors >&2) | custom_ansi_colors | ./scripts/add_log_tips.sh 12 | -------------------------------------------------------------------------------- /bundle/plugins/extract_css.cjs: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | 3 | module.exports = new MiniCssExtractPlugin({ 4 | filename: '[name].[contenthash:8].css', 5 | chunkFilename: '[name].[contenthash:8].css', 6 | // See https://github.com/webpack-contrib/mini-css-extract-plugin#ignoreOrder 7 | ignoreOrder: true, 8 | }) 9 | -------------------------------------------------------------------------------- /app/types/declarations/globals.d.ts: -------------------------------------------------------------------------------- 1 | // This is the result of trial and errors, rather than a clear understanding of how to declare global object types 2 | 3 | // There might be a better/more idomatic way 4 | declare namespace L {} 5 | 6 | interface Window { 7 | app: unknown 8 | opera: unknown 9 | _paq: unknown[] 10 | prerenderReady: boolean 11 | } 12 | -------------------------------------------------------------------------------- /bundle/plugins/detect_circular_dependencies.cjs: -------------------------------------------------------------------------------- 1 | // See https://github.com/aackerman/circular-dependency-plugin 2 | const CircularDependencyPlugin = require('circular-dependency-plugin') 3 | 4 | module.exports = new CircularDependencyPlugin({ 5 | include: /\.js$/, 6 | exclude: /node_modules/, 7 | failOnError: true, 8 | cwd: process.cwd(), 9 | }) 10 | -------------------------------------------------------------------------------- /app/api/oauth.ts: -------------------------------------------------------------------------------- 1 | import type { RelativeUrl } from '#server/types/common' 2 | import { getEndpointPathBuilders } from './endpoint.ts' 3 | 4 | const { action } = getEndpointPathBuilders('oauth/clients') 5 | 6 | export default { 7 | authorize: '/api/oauth/authorize' as RelativeUrl, 8 | clients: { 9 | byId: id => action('by-ids', { ids: id }), 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /app/init_polyfills.ts: -------------------------------------------------------------------------------- 1 | // Most polyfills are handled by babel/core-js (see bundle/rules/babel.cjs) 2 | // but some could not be handled this way 3 | 4 | export async function initPolyfills () { 5 | if (window.visualViewport == null) { 6 | await import('#vendor/visual_viewport_polyfill') 7 | } 8 | } 9 | 10 | export const waitingForPolyfills = initPolyfills() 11 | -------------------------------------------------------------------------------- /app/modules/general/scss/_label.scss: -------------------------------------------------------------------------------- 1 | label, legend{ 2 | color: $label-grey; 3 | display: block; 4 | font-weight: normal; 5 | margin-block-end: 0; 6 | } 7 | 8 | label{ 9 | font-size: 0.9rem; 10 | line-height: 1.5rem; 11 | } 12 | 13 | legend{ 14 | .title{ 15 | font-size: 1rem; 16 | } 17 | .description{ 18 | font-size: 0.9rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bundle/rules/ts.cjs: -------------------------------------------------------------------------------- 1 | module.exports = mode => { 2 | const rule = { 3 | test: /\.ts$/, 4 | resolve: { 5 | // Allow to import modules without specifying the '.js': 6 | // import './foo.js' => import './foo' 7 | // fullySpecified: false, 8 | }, 9 | } 10 | 11 | rule.use = [ require('./babel.cjs')(mode) ] 12 | 13 | return rule 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/allow_persistant_query.ts: -------------------------------------------------------------------------------- 1 | const alwaysKeep = () => true 2 | 3 | const redirectTest = section => allowRedirectPersistantQuery.includes(section) 4 | 5 | const allowRedirectPersistantQuery = [ 6 | 'signup', 7 | 'login', 8 | 'authorize', 9 | ] 10 | 11 | export default { 12 | debug: alwaysKeep, 13 | lang: alwaysKeep, 14 | redirect: redirectTest, 15 | } 16 | -------------------------------------------------------------------------------- /app/modules/general/scss/_case.scss: -------------------------------------------------------------------------------- 1 | // For some reason, ::first-letter doesn't apply to a element, which is thus omitted here 2 | .button, label{ 3 | &:not(.respect-case)::first-letter{ 4 | text-transform: uppercase; 5 | } 6 | } 7 | 8 | .subheader::first-letter{ 9 | text-transform: uppercase; 10 | } 11 | 12 | .uppercased{ 13 | text-transform: uppercase; 14 | } 15 | -------------------------------------------------------------------------------- /app/modules/tasks/lib/get_next_task.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import preq from '#app/lib/preq' 3 | 4 | export async function getNextTask (params) { 5 | const { entitiesType, offset, type } = params 6 | const { tasks } = await preq.get(API.tasks.byEntitiesType({ 7 | type, 8 | 'entities-type': entitiesType, 9 | offset, 10 | })) 11 | return tasks[0] 12 | } 13 | -------------------------------------------------------------------------------- /app/modules/user/lib/oauth.ts: -------------------------------------------------------------------------------- 1 | export function getRequestedAccessRights (scope) { 2 | return scope 3 | .split(/[+\s]/) 4 | .map(accessRight => ({ 5 | key: accessRight, 6 | label: accessRightCustomLabels[accessRight] || accessRight, 7 | })) 8 | } 9 | 10 | const accessRightCustomLabels = { 11 | email: 'access your email address', 12 | 'stable-username': 'access your username', 13 | } 14 | -------------------------------------------------------------------------------- /app/api/images.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointPathBuilders } from './endpoint.ts' 2 | 3 | const { action } = getEndpointPathBuilders('images') 4 | 5 | export default { 6 | upload (container, hash) { return action('upload', { container, hash }) }, 7 | convertUrl: action('convert-url'), 8 | dataUrl (url) { return action('data-url', { url: encodeURIComponent(url) }) }, 9 | gravatar: action('gravatar'), 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #172c41 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/lib/components/actions/autosize.ts: -------------------------------------------------------------------------------- 1 | import _autosize from 'autosize' 2 | 3 | export function autosize (node) { 4 | // Automatically detects changes resulting from user input 5 | // but it fails to detect changes triggered by JS setting the textarea value 6 | _autosize(node) 7 | // Callin the update function doesn't seem to work 8 | // node.addEventListener('change', () => _autosize.update(node)) 9 | } 10 | -------------------------------------------------------------------------------- /app/modules/entities/components/language_label.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if uri} 11 | 12 | {:else} 13 | {lang} 14 | {/if} 15 | -------------------------------------------------------------------------------- /scripts/lint_fix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if [ "$1" != "" ] ; then 3 | eslint --config .eslintrc.cli.cjs --format codeframe --ext js,ts,svelte --fix "$@" 4 | stylelint --config .stylelintrc.cjs --quiet-deprecation-warnings --fix "$@" 5 | else 6 | eslint --config .eslintrc.cli.cjs --format codeframe --ext js,ts,svelte --fix app tests scripts 7 | stylelint --config .stylelintrc.cjs --quiet-deprecation-warnings --fix app 8 | fi 9 | -------------------------------------------------------------------------------- /app/initialize.ts: -------------------------------------------------------------------------------- 1 | import '#general/scss/base.scss' 2 | import '#app/lib/env_config' 3 | import initUnhandledErrorLogger from '#app/lib/unhandled_error_logger' 4 | import initApp from './init_app.ts' 5 | import { waitingForPolyfills } from './init_polyfills.ts' 6 | 7 | // Init handler error before the app so that it can catch any error happenig there 8 | initUnhandledErrorLogger() 9 | 10 | waitingForPolyfills.then(initApp) 11 | -------------------------------------------------------------------------------- /app/modules/general/scss/_shortcut_tip.scss: -------------------------------------------------------------------------------- 1 | .shortcut-tip{ 2 | color: grey; 3 | font-size: 0.9em; 4 | padding: 0; 5 | margin: 0; 6 | text-align: end; 7 | padding-block-start: 0.2em; 8 | padding-block-end: 0.2em; 9 | margin-inline-end: 1em; 10 | 11 | /*Small screens*/ 12 | @media screen and (width < $small-screen) { 13 | // doing shortcuts in mobile is quite hard 14 | display: none; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/lib/type_of.ts: -------------------------------------------------------------------------------- 1 | import { isNull, isNaN, isArray } from 'underscore' 2 | 3 | export default function typeOf (obj?: unknown) { 4 | // just handling what differes from typeof 5 | const type = typeof obj 6 | if (type === 'object') { 7 | if (isNull(obj)) return 'null' 8 | if (isArray(obj)) return 'array' 9 | } 10 | if (type === 'number') { 11 | if (isNaN(obj)) return 'NaN' 12 | } 13 | return type 14 | } 15 | -------------------------------------------------------------------------------- /app/modules/general/lib/prevent_form_submit.ts: -------------------------------------------------------------------------------- 1 | import { currentRoute } from '#app/lib/location' 2 | 3 | const routeAllowlist = [ 4 | 'signup', 5 | 'login', 6 | 'login/reset-password', 7 | ] 8 | 9 | export function preventFormSubmit (e) { 10 | // Allow submit on singup and login to let password managers react to the submit event 11 | if (routeAllowlist.includes(currentRoute())) return 12 | e.preventDefault() 13 | } 14 | -------------------------------------------------------------------------------- /app/modules/inventory/components/importer/external_shelf.svelte: -------------------------------------------------------------------------------- 1 | 4 | 8 | 17 | -------------------------------------------------------------------------------- /app/modules/inventory/components/importer/lib/importers_helpers.ts: -------------------------------------------------------------------------------- 1 | import { compact } from 'underscore' 2 | import { getIsbnData } from '#inventory/lib/importer/extract_isbns' 3 | 4 | export function getInvalidIsbnsString (isbns: string[]) { 5 | const isbnsData = isbns.map(isbn => { 6 | const isbnData = getIsbnData(isbn) 7 | if (isbnData.isInvalid) return isbn 8 | }) 9 | return compact(isbnsData).join(', ') 10 | } 11 | -------------------------------------------------------------------------------- /app/modules/inventory/scss/_shelves_selectors.scss: -------------------------------------------------------------------------------- 1 | label{ 2 | color: $default-text-color; 3 | @include bg-hover($off-white, 5%); 4 | background-color: inherit; 5 | @include display-flex(row, center, flex-start); 6 | @include radius; 7 | cursor: pointer; 8 | margin-block-end: 0.1em; 9 | } 10 | input[type="checkbox"]{ 11 | margin: 0.5em; 12 | // Prevent flex shrink on iOS 16 browsers 13 | flex: 0 0 auto; 14 | } 15 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | ESLINT_ARGS="--format codeframe --ext js,ts,svelte --config .eslintrc.cli.cjs" 6 | 7 | if [ "$1" != "" ] ; then 8 | eslint $ESLINT_ARGS "$@" 9 | stylelint --config .stylelintrc.cjs --quiet-deprecation-warnings --allow-empty-input "$@" 10 | else 11 | eslint $ESLINT_ARGS app tests scripts 12 | stylelint --config .stylelintrc.cjs --quiet-deprecation-warnings app 13 | fi 14 | -------------------------------------------------------------------------------- /app/modules/entities/scss/_entity_list_compact.scss: -------------------------------------------------------------------------------- 1 | .entity-list-compact{ 2 | @include display-flex(row, center); 3 | background-color: white; 4 | margin-block-end: 0.5em; 5 | padding: 0.5em; 6 | inline-size: 100%; 7 | .title{ 8 | flex: 4 0 0; 9 | margin: 0 0.5em; 10 | } 11 | .date{ 12 | flex: 1 0 0; 13 | } 14 | // .publisher{ 15 | // flex: 2 0 0; 16 | // } 17 | .authors{ 18 | flex: 2 0 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/modules/groups/scss/group_settings_commons.scss: -------------------------------------------------------------------------------- 1 | @mixin group-settings-checkbox-commons(){ 2 | input[type="checkbox"]{ 3 | margin: 0.5em; 4 | } 5 | .warning{ 6 | text-align: center; 7 | background-color: $grey; 8 | color: white; 9 | @include radius; 10 | margin-block: 0.5em; 11 | padding-block: 0.5em; 12 | &.light{ 13 | background-color: white; 14 | color: $dark-grey; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bundle/plugins/dynamic_html_index_plugin.cjs: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | 3 | // See https://github.com/jantimon/html-webpack-plugin#options 4 | module.exports = new HtmlWebpackPlugin({ 5 | title: 'Inventaire', 6 | template: 'app/index.html', 7 | minify: { 8 | // See https://github.com/terser/html-minifier-terser#options-quick-reference 9 | removeComments: true, 10 | }, 11 | showErrors: true, 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/githooks/post-checkout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | CHANGED=$(git diff $1 $2 --stat package-lock.json) 3 | if [[ -n $CHANGED ]] 4 | then 5 | echo -e "\e[0;33m△ Warning: package-lock.json has changed between ${1:0:7} and ${2:0:7}" 6 | # Use diff on package.json rather than package-lock.json as the later is too verbose 7 | git --no-pager diff -U0 $1 $2 package.json 8 | echo -e "\e[0;33m△ You may want to run 'npm install'.\e[0m" 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/sitemaps/wrap_urls.js: -------------------------------------------------------------------------------- 1 | export default function (urlsNodes) { 2 | const text = urlsNodes.join('') 3 | return ` 4 | 9 | ${text} 10 | ` 11 | } 12 | -------------------------------------------------------------------------------- /app/modules/general/scss/panel_utils.scss: -------------------------------------------------------------------------------- 1 | @mixin panel($bg-color:$contrast){ 2 | @include shy-border; 3 | background-color: $bg-color; 4 | @include radius; 5 | padding: 0.8em 1em 0.8em 1em; 6 | /*Very small screens*/ 7 | @media screen and (width < $very-small-screen) { 8 | margin: 0.2em 0 0.3em 0; 9 | } 10 | /*Medium to Large screens*/ 11 | @media screen and (width >= $very-small-screen) { 12 | margin: 0.4em 0 0.6em 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/sitemaps/generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import chalk from 'tiny-chalk' 3 | import { rmFiles, generateMainSitemap, mkdirp } from './files_commands.js' 4 | import { generateIndex } from './generate_index.js' 5 | import { generateSitemaps } from './generate_sitemaps.js' 6 | 7 | rmFiles() 8 | mkdirp() 9 | generateMainSitemap() 10 | 11 | generateSitemaps() 12 | .then(generateIndex) 13 | .catch(err => console.log(chalk.red('global err'), err.stack)) 14 | -------------------------------------------------------------------------------- /bundle/plugins/detect_unused_files.cjs: -------------------------------------------------------------------------------- 1 | // See https://github.com/MatthieuLemoine/unused-webpack-plugin 2 | const path = require('node:path') 3 | const UnusedWebpackPlugin = require('unused-webpack-plugin') 4 | 5 | module.exports = new UnusedWebpackPlugin({ 6 | // Source directories 7 | directories: [ 8 | path.resolve(__dirname, '../../app'), 9 | ], 10 | exclude: [ 11 | 'assets/*', 12 | '*.d.ts', 13 | 'tsconfig.client.json', 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /app/modules/general/scss/_colors.scss: -------------------------------------------------------------------------------- 1 | .secondary{ 2 | color: lighten($primary-color, 10%) !important; 3 | } 4 | 5 | .grey{ 6 | @include color-class($grey); 7 | } 8 | 9 | .light-grey{ 10 | @include color-class($light-grey); 11 | } 12 | 13 | .dark-grey{ 14 | @include color-class($dark-grey); 15 | } 16 | .light-blue{ 17 | @include color-class($light-blue); 18 | } 19 | 20 | .white{ 21 | color: #fff; 22 | } 23 | 24 | .contrast{ 25 | background-color: $contrast; 26 | } 27 | -------------------------------------------------------------------------------- /app/modules/general/components/image_div.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {#if url} 9 | 10 | {/if} 11 |
12 | 13 | 21 | -------------------------------------------------------------------------------- /app/modules/entities/scss/title_tip.scss: -------------------------------------------------------------------------------- 1 | @import '#general/scss/utils'; 2 | 3 | .tip{ 4 | /*Large screens*/ 5 | @media screen and (width >= $smaller-screen) { 6 | position: absolute; 7 | inset-block-start: 100%; 8 | inset-inline-start: 0; 9 | inset-inline-end: 0; 10 | z-index: 1; 11 | } 12 | padding: 0.4em 0.6em; 13 | padding-inline-start: 0.6em; 14 | color: #666; 15 | background-color: #eee; 16 | :global(a){ 17 | text-decoration: underline; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/modules/users/invitations.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import { treq } from '#app/lib/preq' 3 | import type { PostInvitationsByEmailsResponse } from '#server/controllers/invitations/by_emails' 4 | import type { GroupId } from '#server/types/group' 5 | 6 | export function sendEmailInvitations ({ emails, message, group }: { emails: string[], message: string, group: GroupId }) { 7 | return treq.post(API.invitations.byEmails, { emails, message, group }) 8 | } 9 | -------------------------------------------------------------------------------- /app/types/declarations/assets.d.ts: -------------------------------------------------------------------------------- 1 | // See https://webpack.js.org/guides/typescript/#importing-other-assets 2 | 3 | declare module '*.svg' { 4 | const content: string 5 | export default content 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string 10 | export default content 11 | } 12 | 13 | declare module '*.css' { 14 | const content: string 15 | export default content 16 | } 17 | 18 | declare module '*.scss' { 19 | const content: string 20 | export default content 21 | } 22 | -------------------------------------------------------------------------------- /app/modules/entities/components/contributions_counts.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | 7 | 8 |
9 | 16 | -------------------------------------------------------------------------------- /scripts/typescript/check_types.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Do not set -e as `svelte-check` is likely to exit with a non-zero code that can be ignored 4 | set -u 5 | 6 | mkdir -p logs 7 | 8 | log_file="logs/types_check_results.log" 9 | 10 | svelte-check --tsconfig ./tsconfig.client.json | 11 | tee "$log_file" 12 | 13 | error_count=$(grep -E '^Error:' "$log_file" --count) 14 | 15 | if [ "$error_count" != 0 ]; then 16 | echo "These logs have been copied in file://./$log_file" 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /app/modules/entities/components/lib/edition_layout_helpers.ts: -------------------------------------------------------------------------------- 1 | import { getSubEntities } from '#entities/components/lib/entities' 2 | 3 | export async function addWorksEditions (works) { 4 | await Promise.all(works.map(addWorkEditions)) 5 | } 6 | 7 | async function addWorkEditions (work) { 8 | work.editions = await getSubEntities('work', work.uri) 9 | } 10 | 11 | export const isOtherEditionWithCover = currentEdition => edition => { 12 | return (edition.uri !== currentEdition.uri) && edition.image 13 | } 14 | -------------------------------------------------------------------------------- /app/modules/settings/lib/notifications_settings_list.ts: -------------------------------------------------------------------------------- 1 | export const notificationsList = [ 2 | // GLOBAL 3 | 'global', 4 | 5 | // NEWS 6 | // 'newsletters' 7 | 'inventories_activity_summary', 8 | 9 | // NETWORK 10 | 'friend_accepted_request', 11 | 'friendship_request', 12 | 'group_invite', 13 | 'group_acceptRequest', 14 | 'group_join_request', 15 | 16 | // TRANSACTIONS 17 | 'your_item_was_requested', 18 | 'update_on_your_item', 19 | 'update_on_item_you_requested', 20 | ] as const 21 | -------------------------------------------------------------------------------- /tests/utils/mock_browser_env.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | 3 | const { inventaireServerHost } = config 4 | 5 | // @ts-expect-error TS2339 6 | globalThis.window ??= {} 7 | // @ts-expect-error TS2339 8 | globalThis.document ??= {} 9 | // @ts-expect-error 10 | globalThis.location ??= { 11 | origin: 'http://localhost:9999', 12 | } 13 | 14 | const nodeFetch = globalThis.fetch 15 | 16 | globalThis.fetch = (url, body) => { 17 | url = `${inventaireServerHost}${url}` 18 | return nodeFetch(url, body) 19 | } 20 | -------------------------------------------------------------------------------- /app/modules/user/lib/password_tests.ts: -------------------------------------------------------------------------------- 1 | import { pass } from '#general/lib/forms' 2 | 3 | const passwordTests = { 4 | 'password should be 8 characters minimum' (password = '') { return password.length < 8 }, 5 | 'password should be 5000 characters maximum' (password = '') { return password.length > 5000 }, 6 | } 7 | 8 | export function testPassword (password) { 9 | return pass({ 10 | value: password, 11 | tests: passwordTests, 12 | }) 13 | } 14 | 15 | export default { 16 | pass: testPassword, 17 | } 18 | -------------------------------------------------------------------------------- /scripts/lint_staged: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Keep A (added) and M (modified) files, if staged (no space before) 6 | staged=$(git status --porcelain | grep -E "^(A|M).*\.(js|ts|svelte)$" | awk '{print $2}') 7 | 8 | export ESLINT_PRE_COMMIT_EXTRA=1 9 | 10 | if [ -z "$staged" ]; then 11 | echo 'no file to lint' 12 | else 13 | if [ "$1" = "fix" ] || [ "$1" = "--fix" ]; then 14 | echo $staged | xargs npm run lint-fix 15 | else 16 | echo $staged | xargs npm run lint 17 | fi 18 | fi 19 | -------------------------------------------------------------------------------- /bundle/rules/postcss.cjs: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env')() 2 | // eslint-disable-next-line import/order 3 | const cssnano = require('cssnano')({ 4 | preset: [ 5 | 'default', 6 | { 7 | discardComments: { 8 | removeAll: true, 9 | }, 10 | }, 11 | ], 12 | }) 13 | 14 | module.exports = { 15 | loader: 'postcss-loader', 16 | options: { 17 | postcssOptions: { 18 | plugins: [ 19 | postcssPresetEnv, 20 | cssnano, 21 | ], 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /app/modules/general/scss/_counter.scss: -------------------------------------------------------------------------------- 1 | @mixin counter-commons(){ 2 | background-color: $secondary-color; 3 | color: $primary-color; 4 | font-weight: bold; 5 | border-radius: 2px; 6 | min-width: 1.2em; 7 | text-align: center; 8 | padding: 0 0.2em 0.1em 0.2em; 9 | &:empty{ 10 | display: none; 11 | } 12 | } 13 | 14 | @mixin hide-counter(){ 15 | // hidding counter when selected 16 | // so that the attention can go to counters 17 | // in the selected layout 18 | .counter{ 19 | display: none; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/sitemaps/write_sitemap.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { promisify } from 'node:util' 3 | import chalk from 'tiny-chalk' 4 | 5 | const writeFile = promisify(fs.writeFile) 6 | const { grey, red, green } = chalk 7 | 8 | export default function (path, content) { 9 | console.log(grey('writting sitemap'), path) 10 | return writeFile(path, content, err => { 11 | if (err != null) { 12 | return console.log(red('err'), err) 13 | } else { 14 | return console.log(green('done!')) 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /app/modules/general/scss/_img.scss: -------------------------------------------------------------------------------- 1 | img{ 2 | max-width: 100%; 3 | height: auto; 4 | display: inline-block; 5 | vertical-align: middle; 6 | // prevent images' alt text to overflow 7 | overflow: hidden; 8 | &.icon{ 9 | max-height: 1.5em; 10 | opacity: 0.8; 11 | } 12 | } 13 | 14 | figure{ 15 | margin:0; 16 | display: inline-block; 17 | figcaption{ 18 | min-height: 3em; 19 | h5{ 20 | color: #fff; 21 | text-align: center; 22 | margin: 0; 23 | padding: 0.8rem; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/key_events.ts: -------------------------------------------------------------------------------- 1 | export function getActionKey (e) { 2 | const key = e.which || e.keyCode 3 | return actionKeysMap[key] 4 | } 5 | 6 | const actionKeysMap = { 7 | 9: 'tab', 8 | 13: 'enter', 9 | 16: 'shift', 10 | 17: 'ctrl', 11 | 18: 'alt', 12 | 27: 'esc', 13 | 33: 'pageup', 14 | 34: 'pagedown', 15 | 35: 'end', 16 | 36: 'home', 17 | 37: 'left', 18 | 38: 'up', 19 | 39: 'right', 20 | 40: 'down', 21 | } 22 | 23 | export function stopEscPropagation (e) { 24 | if (getActionKey(e) === 'esc') e.stopPropagation() 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/components/actions/resize_observer.ts: -------------------------------------------------------------------------------- 1 | import { assertFunction } from '#app/lib/assert_types' 2 | 3 | export function resizeObserver (node, options) { 4 | const { onElementResize } = options 5 | assertFunction(onElementResize) 6 | 7 | const resizeObserver = new ResizeObserver(() => { 8 | // console.log('height changed:', entries[0].target.clientHeight) 9 | onElementResize() 10 | }) 11 | 12 | resizeObserver.observe(node) 13 | 14 | return { 15 | destroy () { 16 | resizeObserver.disconnect() 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/modules/groups/lib/group_actions_alt.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import preq from '#app/lib/preq' 3 | import type { GroupId } from '#server/types/group' 4 | import type { UserId } from '#server/types/user' 5 | 6 | export async function groupAction ({ action, groupId, userId }: { action: string, groupId: GroupId, userId?: UserId }) { 7 | const res = await preq.put(API.groups.base, { 8 | action, 9 | group: groupId, 10 | // Required only for actions implying an other user 11 | user: userId, 12 | }) 13 | return res 14 | } 15 | -------------------------------------------------------------------------------- /tests/libs/type_of.ts: -------------------------------------------------------------------------------- 1 | import 'should' 2 | import typeOf from '#app/lib/type_of' 3 | 4 | describe('typeOf', () => { 5 | it('should return the right type', () => { 6 | typeOf('hello').should.equal('string') 7 | typeOf([ 'hello' ]).should.equal('array') 8 | typeOf({ hel: 'lo' }).should.equal('object') 9 | typeOf(83110).should.equal('number') 10 | typeOf(null).should.equal('null') 11 | typeOf().should.equal('undefined') 12 | typeOf(false).should.equal('boolean') 13 | typeOf(Number('boudu')).should.equal('NaN') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /tests/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util' 2 | 3 | export const shouldNotBeCalled = res => { 4 | console.warn(inspect(res, false, null), 'undesired positive res') 5 | const err = new Error('function was expected not to be called') 6 | Object.assign(err, { 7 | // Give 'shouldNotBeCalled' more chance to appear in the red text of the failing test 8 | name: 'shouldNotBeCalled', 9 | statusCode: 'shouldNotBeCalled', 10 | body: { status_verbose: 'shouldNotBeCalled' }, 11 | context: { res }, 12 | }) 13 | throw err 14 | } 15 | -------------------------------------------------------------------------------- /scripts/build_and_save_logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This can unfortunately not be done from the package.json script 3 | # as it triggers "sh: 1: set: Illegal option -o pipefail" 4 | # which is even worse that what is described here https://github.com/npm/npm/issues/18517 5 | set -eo pipefail 6 | 7 | mkdir -p logs 8 | 9 | echo -e "\e[0;32mbuild and save logs\e[0m" 10 | ./scripts/build | tee ./logs/build.log 11 | echo -e "\e[0;32mbuild and save logs: done\e[0m" 12 | 13 | # Exit with code 0 (apparently required for Docker image build to succeed) 14 | true 15 | -------------------------------------------------------------------------------- /app/lib/reload_once_a_day.ts: -------------------------------------------------------------------------------- 1 | import { property } from 'underscore' 2 | // Reload the page every 24 hours to make sure we have the latest version 3 | // unless the current page is being edited 4 | 5 | export default () => setInterval(tryReload, 24 * 60 * 60 * 1000) 6 | 7 | function tryReload () { 8 | if (textareaContentLength() > 0) return 9 | return window.location.reload() 10 | } 11 | 12 | function textareaContentLength () { 13 | return Array.from(document.querySelectorAll('textarea')) 14 | .map(property('value')) 15 | .join('') 16 | .length 17 | } 18 | -------------------------------------------------------------------------------- /app/modules/general/scss/_links.scss: -------------------------------------------------------------------------------- 1 | a{ 2 | line-height: inherit; 3 | text-decoration: none; 4 | @include text-hover($primary-color, darken($primary-color, 5%)); 5 | word-break: break-word; 6 | &:hover{ 7 | cursor: pointer; 8 | } 9 | } 10 | 11 | .classic-link{ 12 | text-decoration: underline; 13 | @include text-hover(rgb(0, 0, 238), rgb(49, 156, 194)); 14 | } 15 | 16 | .link{ 17 | @include link-underline-on-hover($dark-grey, $link-hover-grey); 18 | } 19 | 20 | .content-link{ 21 | @include link-underline-on-hover($grey, lighten($grey, 5%)); 22 | } 23 | -------------------------------------------------------------------------------- /.mocharc.cjs: -------------------------------------------------------------------------------- 1 | const nodeOptionsBeforeV20 = [ 2 | 'loader=tsx/esm', 3 | // Mute node error: (node:29544) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()` 4 | 'no-warnings', 5 | ] 6 | 7 | const nodeOptionsFromV20 = [ 8 | 'import=tsx/esm', 9 | ] 10 | 11 | const nodeVersion = parseInt(process.version.split('.')[0].slice(1)) 12 | 13 | module.exports = { 14 | extension: 'ts', 15 | 'node-option': nodeVersion >= 20 ? nodeOptionsFromV20 : nodeOptionsBeforeV20, 16 | require: './tests/utils/mock_browser_env.ts', 17 | } 18 | -------------------------------------------------------------------------------- /app/lib/encoding_errors.ts: -------------------------------------------------------------------------------- 1 | const encodingsErrors = { 2 | // characters showing that this encoding should be used instead 3 | 'é': 'utf-8', 4 | 'è': 'utf-8', 5 | 'ô': 'utf-8', 6 | // Using this hack to avoid getting the file falsly identified as a binary file (TS1490) 7 | [decodeURIComponent('%EF%BF%BD')]: 'ISO-8859-1', 8 | } 9 | 10 | const encodingsErrorsList = Object.keys(encodingsErrors) 11 | 12 | export function testEncodingErrors (text: string) { 13 | for (const err of encodingsErrorsList) { 14 | if (text.match(err)) return encodingsErrors[err] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/modules/entities/components/lib/claim_layout_helpers.ts: -------------------------------------------------------------------------------- 1 | import { infoboxPropertiesByType } from '#entities/components/lib/claims_helpers' 2 | import { pluralize } from '#entities/lib/types/entities_types' 3 | 4 | export const getSubentitiesTypes = property => { 5 | const subentitiesTypes = [] 6 | Object.keys(infoboxPropertiesByType).forEach(type => { 7 | const typeProps = infoboxPropertiesByType[type] 8 | if (typeProps.includes(property) && type !== 'article') { 9 | subentitiesTypes.push(pluralize(type)) 10 | } 11 | }) 12 | return subentitiesTypes 13 | } 14 | -------------------------------------------------------------------------------- /app/modules/entities/components/patches/image_preview.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {i18n('Image 12 |

{imageHash}

13 |
14 | -------------------------------------------------------------------------------- /bundle/rules/css.cjs: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const postCss = require('./postcss.cjs') 3 | 4 | module.exports = mode => { 5 | const rule = { 6 | test: /\.css$/, 7 | } 8 | 9 | if (mode === 'production') { 10 | rule.use = [ 11 | MiniCssExtractPlugin.loader, 12 | { loader: 'css-loader', options: { importLoaders: 1 } }, 13 | postCss, 14 | ] 15 | } else { 16 | rule.use = [ 17 | { loader: 'style-loader' }, 18 | { loader: 'css-loader', options: { importLoaders: 0 } }, 19 | ] 20 | } 21 | 22 | return rule 23 | } 24 | -------------------------------------------------------------------------------- /svelte.config.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const svelteWebpackConfig = require('./bundle/rules/svelte.cjs')('dev') 3 | 4 | const { preprocess } = svelteWebpackConfig[0].use[0].options 5 | 6 | // Share the preprocess config with the svelte-vscode extension, to be able to lint scss in vscode 7 | // The extension relies on the language-server, see: 8 | // https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/in-general.md 9 | // https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/scss-less.md 10 | module.exports = { preprocess } 11 | -------------------------------------------------------------------------------- /app/modules/general/scss/_fonts_and_typo_utils.scss: -------------------------------------------------------------------------------- 1 | $serif: 'Alegreya'; 2 | $sans-serif: 'Alegreya Sans'; 3 | 4 | $header-font-family: $serif, serif; 5 | $body-font-family: $sans-serif, 'Helvetica Neue', Helvetica, Arial, sans-serif; 6 | 7 | $topbar-link-font-family: $header-font-family; 8 | $tabs-navigation-font-family: $header-font-family; 9 | $body-font-color: #222; 10 | 11 | $button-font-family: $header-font-family; 12 | $input-font-family: $sans-serif; 13 | $label-font-family: $sans-serif; 14 | 15 | @mixin serif() { 16 | font-family: $serif; 17 | } 18 | @mixin sans-serif() { 19 | font-family: $sans-serif; 20 | } 21 | -------------------------------------------------------------------------------- /scripts/githooks/pre-merge-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Prevent merge if remote and local commits subjects do not match 3 | set -eu 4 | 5 | echo -e "\e[0;30mchecking origin remote sync\e[0m" 6 | branch=$(echo $GIT_REFLOG_ACTION| cut -f 2- -d ' ') 7 | 8 | git fetch --quiet origin $branch 9 | 10 | # Log commit subject differences 11 | # to be able to spot similar commits despite a possible rebase hashes rewrite 12 | commits_messages_diffs=$(git log --pretty=%s $branch..origin/$branch) 13 | 14 | if [ "$commits_messages_diffs" ]; then 15 | echo "$branch branch does not look up to date with its remote." 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /app/api/commons.ts: -------------------------------------------------------------------------------- 1 | import { buildPath } from '#app/lib/location' 2 | import { truncateDecimals } from '#map/lib/geo' 3 | 4 | export default { 5 | search (base, text) { 6 | return buildPath(base, { 7 | action: 'search', 8 | search: encodeURIComponent(text), 9 | }) 10 | }, 11 | 12 | searchByPosition (base, bbox) { 13 | return buildPath(base, { 14 | action: 'search-by-position', 15 | // don't let buildPath do the bbox stringification 16 | // as it would uses a simple bbox.toString() and lose the [] 17 | bbox: JSON.stringify(bbox.map(truncateDecimals)), 18 | }) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /app/modules/user/lib/request_logout.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import { localStorageProxy } from '#app/lib/local_storage' 3 | import log_ from '#app/lib/loggers' 4 | import preq from '#app/lib/preq' 5 | 6 | export default redirect => { 7 | preq.post(API.auth.logout) 8 | .then(logoutSuccess(redirect)) 9 | .catch(log_.Error('logout error')) 10 | } 11 | 12 | const logoutSuccess = redirect => function () { 13 | // Clearing localstorage 14 | localStorageProxy.clear() 15 | log_.info('You have been successfully logged out') 16 | // Default to redirecting home 17 | window.location.href = redirect || '/' 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # NPM packages folder. 14 | node_modules/ 15 | npm-debug.log* 16 | tmp/ 17 | .status 18 | .commit 19 | *.sublime* 20 | *.gz 21 | .*cache 22 | *.tsbuildinfo 23 | 24 | public/* 25 | dist 26 | old-* 27 | app/assets/fonts 28 | config/local* 29 | docs 30 | 31 | scripts/assets/weight_history 32 | scripts/assets/bundle_reports 33 | scripts/local 34 | app/assets/js 35 | inventaire-i18n 36 | issues 37 | scripts/assets/bundles_stats/ 38 | scripts/assets/bundles_archives/ 39 | vendor/ 40 | -------------------------------------------------------------------------------- /app/config.ts: -------------------------------------------------------------------------------- 1 | import preq from '#app/lib/preq' 2 | import type { ClientConfig } from '#server/controllers/config' 3 | 4 | // Do not use API.config or getEndpointBase to avoid a circular dependency 5 | const configEndpoint = '/api/config' 6 | 7 | export const config: ClientConfig = await preq.get(configEndpoint) 8 | 9 | export let bundleMeta 10 | 11 | try { 12 | bundleMeta = await preq.get('/public/dist/bundle_meta.json') 13 | } catch (err) { 14 | // Known case: if the client was not built 15 | // Using console directly to avoid circular dependencies 16 | console.error('bundle meta fetch error', err) 17 | bundleMeta = {} 18 | } 19 | -------------------------------------------------------------------------------- /app/modules/entities/components/cleanup/language_selector.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /app/modules/search/lib/find_uri.ts: -------------------------------------------------------------------------------- 1 | import { isInvEntityId } from '#app/lib/boolean_tests' 2 | import { looksLikeAnIsbn, normalizeIsbn } from '#app/lib/isbn' 3 | 4 | export default function (text: string) { 5 | text = text.trim() 6 | if (isWikidataId.test(text)) text = 'wd:' + text 7 | if (caseInsensitiveEntityUri.test(text)) return text.replace('wd:q', 'wd:Q') 8 | if (isInvEntityId(text)) return 'inv:' + text 9 | if (looksLikeAnIsbn(text)) return 'isbn:' + normalizeIsbn(text) 10 | } 11 | 12 | const caseInsensitiveEntityUri = /^(wd:[Qq][1-9]\d*|inv:[0-9a-f]{32}|isbn:\w{10}(\w{3})?)$/ 13 | 14 | const isWikidataId = /^[Qq][1-9]\d*/ 15 | -------------------------------------------------------------------------------- /app/modules/entities/lib/create_entity.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import preq from '#app/lib/preq' 3 | import type { EntityDraft } from '#app/types/entity' 4 | import { serializeEntity } from './entities' 5 | 6 | export type EntityDraftWithCreationParams = EntityDraft & { createOnWikidata?: boolean } 7 | 8 | export async function createEntity (params: EntityDraftWithCreationParams) { 9 | const { labels, claims, createOnWikidata } = params 10 | const prefix = createOnWikidata === true ? 'wd' : 'inv' 11 | const entity = await preq.post(API.entities.create, { prefix, labels, claims }) 12 | return serializeEntity(entity) 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/images/wikidata.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/modules/general/scss/base.scss: -------------------------------------------------------------------------------- 1 | // Node modules imports 2 | // Using sass-loader '~' to refer to node_modules 3 | // See https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules 4 | @import '~normalize.css'; 5 | 6 | // Mixins and variables 7 | @import 'utils'; 8 | 9 | @import 'type'; 10 | @import 'colors'; 11 | @import 'fonts_and_typo'; 12 | @import 'links'; 13 | @import 'case'; 14 | @import 'general_settings'; 15 | @import 'loader'; 16 | @import 'buttons'; 17 | @import 'input'; 18 | @import 'select'; 19 | @import 'img'; 20 | @import 'label'; 21 | @import 'modal'; 22 | @import '#entities/scss/entities'; 23 | @import '#map/scss/map'; 24 | -------------------------------------------------------------------------------- /bundle/rules/scss.cjs: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const postCss = require('./postcss.cjs') 3 | 4 | module.exports = mode => { 5 | const rule = { 6 | test: /\.scss$/, 7 | } 8 | 9 | if (mode === 'production') { 10 | rule.use = [ 11 | MiniCssExtractPlugin.loader, 12 | { loader: 'css-loader', options: { importLoaders: 2 } }, 13 | postCss, 14 | ] 15 | } else { 16 | rule.use = [ 17 | { loader: 'style-loader' }, 18 | { loader: 'css-loader', options: { importLoaders: 1 } }, 19 | ] 20 | } 21 | 22 | rule.use.push('sass-loader') 23 | 24 | return rule 25 | } 26 | -------------------------------------------------------------------------------- /app/modules/inventory/components/importer/lib/candidate_row_helpers.ts: -------------------------------------------------------------------------------- 1 | import { guessUriFromIsbn } from '#inventory/lib/importer/import_helpers' 2 | import { mainUser } from '#user/lib/main_user' 3 | 4 | export const getUserExistingItemsPathname = isbnData => { 5 | const uri = guessUriFromIsbn({ isbnData }) 6 | const username = mainUser.username 7 | return `/users/${username}/inventory/${uri}` 8 | } 9 | 10 | export const statusContents = { 11 | newEntry: 'We could not identify this entry in the common bibliographic database. A new entry will be created', 12 | error: 'oops, something wrong happened', 13 | needInfo: 'need more information', 14 | } 15 | -------------------------------------------------------------------------------- /app/modules/entities/components/layouts/entity_claim_link.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if url} 15 | 16 | 22 | {:else} 23 | {value} 24 | {/if} 25 | -------------------------------------------------------------------------------- /app/modules/general/scss/_flex.scss: -------------------------------------------------------------------------------- 1 | @mixin common-flex($direction:null, $align:null, $justify:null, $wrap:null) { 2 | @if $direction { flex-direction: $direction; } 3 | @if $align { align-items: $align; } 4 | @if $justify { justify-content: $justify; } 5 | @if $wrap { flex-wrap: $wrap; } 6 | } 7 | 8 | @mixin display-flex($direction:null, $align:null, $justify:null, $wrap:null) { 9 | display: flex; 10 | @include common-flex($direction, $align, $justify, $wrap); 11 | } 12 | 13 | @mixin display-inline-flex($direction:null, $align:null, $justify:null, $wrap:null) { 14 | display: inline-flex; 15 | @include common-flex($direction, $align, $justify, $wrap); 16 | } 17 | -------------------------------------------------------------------------------- /app/modules/map/lib/geo.ts: -------------------------------------------------------------------------------- 1 | import { assertObjects } from '#app/lib/assert_types' 2 | import leafletLite from './leaflet_lite.ts' 3 | 4 | // Coordinates are returned in decimal degrees 5 | // There is no need to keep more than 4 decimals, cf https://xkcd.com/2170/ 6 | // See https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates 7 | export function truncateDecimals (degree) { 8 | return Math.round(degree * 10000) / 10000 9 | } 10 | 11 | // a, b MUST be { lat, lng } coords objects 12 | export function distanceBetween (a, b) { 13 | assertObjects([ a, b ]) 14 | // return the distance in kilometers 15 | return leafletLite.distance(a, b) / 1000 16 | } 17 | -------------------------------------------------------------------------------- /app/modules/entities/components/patches/entity_claim_value.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if label && lang} 14 | 20 | {label} 21 | 22 | {/if} 23 | -------------------------------------------------------------------------------- /app/modules/inventory/lib/importer/parsers/babelio.ts: -------------------------------------------------------------------------------- 1 | import { trim } from '#app/lib/utils' 2 | 3 | export default obj => ({ 4 | rawEntry: obj, 5 | title: obj.Titre, 6 | authors: obj.Auteur.split(',').map(trim), 7 | isbn: obj.ISBN, 8 | details: obj.Critique, 9 | publicationDate: formatDate(obj), 10 | }) 11 | 12 | const formatDate = obj => { 13 | if (hasValidDate(obj)) { 14 | // Convert 29/02/2012 to 2012-02-29 15 | return obj['Date de publication']?.split('/').reverse().join('-') 16 | } 17 | } 18 | 19 | const hasValidDate = obj => { 20 | // Only exclude what seems to be the default value 21 | return obj['Date de publication'] !== '0000-00-00' 22 | } 23 | -------------------------------------------------------------------------------- /app/api/tasks.ts: -------------------------------------------------------------------------------- 1 | import { forceArray } from '#app/lib/utils' 2 | import { getEndpointPathBuilders } from './endpoint.ts' 3 | 4 | const { action } = getEndpointPathBuilders('tasks') 5 | 6 | export default { 7 | byIds (ids) { return action('by-ids', { ids }) }, 8 | byScore (limit, offset) { return action('by-score', { limit, offset }) }, 9 | byEntitiesType (params) { return action('by-entities-type', params) }, 10 | bySuggestionUris (uris) { 11 | uris = forceArray(uris).join('|') 12 | return action('by-suggestion-uris', { uris }) 13 | }, 14 | deduplicateWorks: action('deduplicate-works'), 15 | update: action('update'), 16 | count: action('tasks-count'), 17 | } 18 | -------------------------------------------------------------------------------- /app/lib/location_store.ts: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store' 2 | import { debounce } from 'underscore' 3 | import { currentRoute, routeSection } from '#app/lib/location' 4 | import { vent } from '#app/radio' 5 | 6 | function getLocationData () { 7 | const route = currentRoute() 8 | return { 9 | route, 10 | absoluteRoute: `/${route}`, 11 | section: routeSection(route), 12 | } 13 | } 14 | 15 | export const locationStore = readable(getLocationData(), set => { 16 | const update = () => set(getLocationData()) 17 | const lazyUpdate = debounce(update, 100) 18 | vent.on('route:change', lazyUpdate) 19 | return () => vent.off('route:change', lazyUpdate) 20 | }) 21 | -------------------------------------------------------------------------------- /app/modules/entities/scss/entity_editors_commons.scss: -------------------------------------------------------------------------------- 1 | @import '#general/scss/utils'; 2 | 3 | .editor-section{ 4 | @include panel; 5 | align-self: stretch; 6 | display: flex; 7 | /*Large screens*/ 8 | @media screen and (width >= $smaller-screen) { 9 | margin: 0.4em 0 0.6em 0; 10 | padding: 1em 1.5em; 11 | } 12 | /*Small screens*/ 13 | @media screen and (width < $smaller-screen) { 14 | flex-direction: column; 15 | padding: 0.4em; 16 | } 17 | } 18 | 19 | .editor-section-header{ 20 | font-size: 1rem; 21 | @include sans-serif; 22 | font-weight: bold; 23 | margin-inline-end: 0.5em; 24 | margin-block-start: 0.5em; 25 | flex: 0 0 auto; 26 | } 27 | -------------------------------------------------------------------------------- /app/api/shelves.ts: -------------------------------------------------------------------------------- 1 | import { forceArray } from '#app/lib/utils' 2 | import { getEndpointPathBuilders } from './endpoint.ts' 3 | 4 | const { base, action } = getEndpointPathBuilders('shelves') 5 | 6 | export default { 7 | base, 8 | byId (id) { return action('by-ids', { ids: id, 'with-items': true }) }, 9 | byIds (ids) { 10 | ids = forceArray(ids).join('|') 11 | return action('by-ids', { ids, 'with-items': true }) 12 | }, 13 | byOwners (id) { return action('by-owners', { owners: id }) }, 14 | addItems: action('add-items'), 15 | removeItems: action('remove-items'), 16 | create: action('create'), 17 | update: action('update'), 18 | delete: action('delete'), 19 | } 20 | -------------------------------------------------------------------------------- /app/modules/general/components/counter.svelte: -------------------------------------------------------------------------------- 1 | 10 |
11 |
12 | {count}/{total} 13 |
14 | {#if spinner} 15 | 16 | {/if} 17 | {#if message} 18 | {I18n(message)}... 19 | {/if} 20 |
21 | 28 | -------------------------------------------------------------------------------- /app/modules/entities/components/layouts/entity_list_compact_title_row.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 | {I18n('date')} 7 |
8 | 9 |
10 | {I18n('title')} 11 |
12 | 13 |
14 | {I18n('authors')} 15 |
16 |
17 | 27 | -------------------------------------------------------------------------------- /app/modules/entities/components/patches/operation_value_reference.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if getUrl} 15 | {#each values as value} 16 | 22 | {/each} 23 | {:else} 24 | 25 | {/if} 26 | -------------------------------------------------------------------------------- /app/modules/inventory/lib/scanner/draw_canvas.ts: -------------------------------------------------------------------------------- 1 | import type { QuaggaJSStatic } from '@ericblade/quagga2' 2 | 3 | const def = { x: 0, y: 1 } 4 | const style = { color: 'green', lineWidth: 2 } 5 | 6 | export function drawCanvasFactory (Quagga: QuaggaJSStatic) { 7 | let alreadyDrawn = false 8 | return function drawCanvas (result) { 9 | if (alreadyDrawn) return 10 | 11 | if (result?.boxes != null) { 12 | const drawingCtx = Quagga.canvas.ctx.overlay 13 | // drawingCanvas = Quagga.canvas.dom.overlay 14 | 15 | const box = result.boxes[0] 16 | Quagga.ImageDebug.drawPath(box, def, drawingCtx, style) 17 | 18 | alreadyDrawn = true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/modules/listings/lib/stores/user_listings.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | import { getListingsByCreators } from '#listings/lib/listings' 3 | import { mainUser } from '#user/lib/main_user' 4 | 5 | const noop = () => {} 6 | 7 | function start (setStoreValue) { 8 | refresh(setStoreValue) 9 | const stop = noop 10 | return stop 11 | } 12 | 13 | async function refresh (setStoreValue) { 14 | if (mainUser) { 15 | const { listings } = await getListingsByCreators({ usersIds: [ mainUser?._id ] }) 16 | setStoreValue(listings) 17 | } else { 18 | setStoreValue([]) 19 | } 20 | } 21 | 22 | export const userListings = Object.assign(writable([], start), { refresh }) 23 | -------------------------------------------------------------------------------- /app/api/data.ts: -------------------------------------------------------------------------------- 1 | import { fixedEncodeURIComponent, forceArray } from '#app/lib/utils' 2 | import { getEndpointPathBuilders } from './endpoint.ts' 3 | 4 | const { action } = getEndpointPathBuilders('data') 5 | 6 | export default { 7 | wikipediaExtract (lang, title) { 8 | title = fixedEncodeURIComponent(title) 9 | return action('wp-extract', { lang, title }) 10 | }, 11 | isbn (isbn) { 12 | return action('isbn', { isbn }) 13 | }, 14 | summaries: ({ uri, langs, refresh }) => { 15 | langs = forceArray(langs).join('|') 16 | return action('summaries', { uri, langs, refresh }) 17 | }, 18 | propertyValues: action('property-values'), 19 | properties: action('properties'), 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/available_lang_list.ts: -------------------------------------------------------------------------------- 1 | import { clone } from 'underscore' 2 | import wdLang from 'wikidata-lang' 3 | import log_ from '#app/lib/loggers' 4 | 5 | export default (availableLangs, selectedLang) => { 6 | return availableLangs 7 | .map(lang => { 8 | let langObj = wdLang.byCode[lang] 9 | if (langObj == null) { 10 | log_.warn(`lang not found in wikidata-lang: ${lang}`) 11 | langObj = { code: lang, label: lang, native: lang } 12 | } 13 | 14 | langObj = clone(langObj) 15 | if (langObj.code === selectedLang) langObj.selected = true 16 | return langObj 17 | }) 18 | .sort(alphabetically) 19 | } 20 | 21 | const alphabetically = (a, b) => a.code > b.code ? 1 : -1 22 | -------------------------------------------------------------------------------- /app/modules/general/scss/_background.scss: -------------------------------------------------------------------------------- 1 | @mixin bg-cover(){ 2 | background-size: cover; 3 | background-position: center center; 4 | transition: background 0.5s ease; 5 | } 6 | 7 | @mixin multidef-bg-cover($filename, $hd:null){ 8 | @include bg-cover; 9 | /*Small screens*/ 10 | @media screen and (width < 500px) { 11 | background-image: url('assets/images/small/' + $filename); 12 | } 13 | /*Large screens*/ 14 | @media screen and (width >= 500px) { 15 | background-image: url('assets/images/medium/' + $filename); 16 | } 17 | @if $hd { 18 | @media screen and (width >= 1300px) { 19 | background-image: url('assets/images/large/' + $filename); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/modules/map/lib/leaflet_lite.ts: -------------------------------------------------------------------------------- 1 | // Set of functions borrowed from Leaflet to work on geographic positions 2 | // when Leaflet itself has not be requested 3 | // Source: https://github.com/Leaflet/Leaflet/blob/a1c1ea214f3077469ace7e9cef2d79225d757c97/src/geo/crs/CRS.Earth.js 4 | 5 | const radius = 6371000 6 | 7 | export default { 8 | distance (latlng1, latlng2) { 9 | const rad = Math.PI / 180 10 | const lat1 = latlng1.lat * rad 11 | const lat2 = latlng2.lat * rad 12 | const a = (Math.sin(lat1) * Math.sin(lat2)) + 13 | (Math.cos(lat1) * Math.cos(lat2) * 14 | Math.cos((latlng2.lng - latlng1.lng) * rad)) 15 | 16 | return radius * Math.acos(Math.min(a, 1)) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /app/modules/settings/scss/common_settings.scss: -------------------------------------------------------------------------------- 1 | @import '#general/scss/utils'; 2 | fieldset{ 3 | padding-block-start: 1.5em; 4 | padding-inline-start: 1.5em; 5 | } 6 | fieldset:first-of-type{ 7 | padding-block-start: 0; 8 | } 9 | h2{ 10 | padding-block-end: 0.5em; 11 | border-bottom: 1px solid #ccc; 12 | margin-block-end: 0.8em; 13 | } 14 | @mixin settings-h3(){ 15 | margin-block-start: 1em; 16 | margin-block-end: 0.2em; 17 | @include sans-serif; 18 | font-size: 1.1rem; 19 | font-weight: bold; 20 | } 21 | 22 | .first-title{ 23 | margin-block-start: 0; 24 | } 25 | 26 | /*Small screens*/ 27 | @media screen and (width < 470px) { 28 | fieldset{ 29 | padding: 1em 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/modules/transactions/lib/cancellable_states.ts: -------------------------------------------------------------------------------- 1 | // after which action should the transaction be displayed as cancellable 2 | const commonOneWay = [ 'accepted' ] 3 | const commonLending = [ 'accepted', 'confirmed' ] 4 | // the owner can't cancel after 'requested', as she can already just 'decline' 5 | // with the exact same effects 6 | const requester = [ 'requested' ] 7 | 8 | const oneWay = { 9 | requester: requester.concat(commonOneWay), 10 | owner: commonOneWay, 11 | } 12 | 13 | const lending = { 14 | requester: requester.concat(commonLending), 15 | owner: commonLending, 16 | } 17 | 18 | export const cancellableStates = { 19 | giving: oneWay, 20 | lending, 21 | selling: oneWay, 22 | } 23 | -------------------------------------------------------------------------------- /app/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { getEndpointPathBuilders } from './endpoint.ts' 2 | 3 | const { action, actionPartial } = getEndpointPathBuilders('auth') 4 | 5 | export default { 6 | usernameAvailability: actionPartial('username-availability', 'username'), 7 | emailAvailability: actionPartial('email-availability', 'email'), 8 | 9 | signup: action('signup'), 10 | login: action('login'), 11 | logout: action('logout'), 12 | resetPassword: action('reset-password'), 13 | 14 | emailConfirmation: action('email-confirmation'), 15 | updatePassword: action('update-password'), 16 | // submit: defined directly in index.html form 17 | 18 | oauth: { 19 | wikidata: action('wikidata-oauth'), 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/string_value_display.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /app/lib/local_storage.ts: -------------------------------------------------------------------------------- 1 | // If localStorage isnt supported (or more probably, blocked), 2 | // replace it by a global object: data won't be persisted 3 | // from one session to the other, but who's fault is that 4 | let _localStorageProxy 5 | try { 6 | window.localStorage.setItem('localStorage-support', 'true') 7 | _localStorageProxy = localStorage 8 | } catch (err) { 9 | console.warn('localStorage isnt supported', err) 10 | let storage = {} 11 | _localStorageProxy = { 12 | getItem (key) { return storage[key] || null }, 13 | setItem (key, value) { 14 | storage[key] = value 15 | }, 16 | clear () { storage = {} }, 17 | } 18 | } 19 | 20 | export const localStorageProxy = _localStorageProxy 21 | -------------------------------------------------------------------------------- /app/modules/entities/lib/entity_refresh.ts: -------------------------------------------------------------------------------- 1 | import log_ from '#app/lib/loggers' 2 | import type { TimeoutId } from '#app/types/common' 3 | import type { EntityUri } from '#server/types/entity' 4 | 5 | const refreshingEntities: Record = {} 6 | 7 | export function startRefreshTimeSpan (uri: EntityUri) { 8 | log_.info(uri, 'start refresh time span') 9 | if (refreshingEntities[uri]) clearTimeout(refreshingEntities[uri]) 10 | refreshingEntities[uri] = setTimeout(() => { 11 | log_.info(uri, 'stop refresh time span') 12 | delete refreshingEntities[uri] 13 | }, 2000) 14 | } 15 | 16 | export function entityDataShouldBeRefreshed (uri: EntityUri) { 17 | return refreshingEntities[uri] != null 18 | } 19 | -------------------------------------------------------------------------------- /app/modules/inventory/components/importer/lib/import_items_helpers.ts: -------------------------------------------------------------------------------- 1 | import { isResolved } from '#inventory/lib/importer/import_helpers' 2 | 3 | export const removeCreatedCandidates = ({ candidates, processedCandidates }) => { 4 | const createdIndices = processedCandidates.map(createdCandidate => { 5 | if (createdCandidate.item) return createdCandidate.index 6 | }) 7 | return candidates.filter(candidate => !createdIndices.includes(candidate.index)) 8 | } 9 | 10 | export const isAlreadyResolved = candidate => { 11 | const authorsResolved = candidate.authors?.every(isResolved) 12 | const worksResolved = candidate.works?.every(isResolved) 13 | return authorsResolved && worksResolved && candidate.edition 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/wikimedia/commons.ts: -------------------------------------------------------------------------------- 1 | import { fixedEncodeURIComponent } from '#app/lib/utils' 2 | import type { AbsoluteUrl } from '#server/types/common' 3 | 4 | // For more complete data (author, license, ...) 5 | // See in the server repo: server/data/commons/thumb.js 6 | export function thumbnail (file, width = 100) { 7 | if (file == null) return 8 | if (!alreadyEncoded(file)) file = fixedEncodeURIComponent(file) 9 | // Example: 10 | // - 2000px-Gallimard,_rue_Gallimard.jpg => Gallimard,_rue_Gallimard.jpg 11 | file = file.replace(/^\d+px-/, '') 12 | return `https://commons.wikimedia.org/wiki/Special:FilePath/${file}?width=${width}` as AbsoluteUrl 13 | } 14 | 15 | const alreadyEncoded = file => file.match(/%[0-9A-F]/) != null 16 | -------------------------------------------------------------------------------- /app/api/groups.ts: -------------------------------------------------------------------------------- 1 | import { fixedEncodeURIComponent } from '#app/lib/utils' 2 | import Commons from './commons.ts' 3 | import { getEndpointPathBuilders } from './endpoint.ts' 4 | 5 | const { base, action } = getEndpointPathBuilders('groups') 6 | 7 | const { 8 | search, 9 | searchByPosition, 10 | } = Commons 11 | 12 | export default { 13 | base, 14 | byId (id) { return action('by-id', { id }) }, 15 | bySlug (slug) { return action('by-slug', { slug }) }, 16 | last: action('last'), 17 | search: search.bind(null, base), 18 | searchByPosition: searchByPosition.bind(null, base), 19 | slug (name, groupId) { 20 | name = fixedEncodeURIComponent(name) 21 | return action('slug', { name, group: groupId }) 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /app/modules/entities/components/entity_label.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | {#await labelPromise then { label }}{label}{/await} 17 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/lib/suggestions/property_values_shortlist.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import { assertString } from '#app/lib/assert_types' 3 | import preq from '#app/lib/preq' 4 | import { pluralize } from '#entities/lib/types/entities_types' 5 | import type { EntityUri } from '#server/types/entity' 6 | 7 | export const allowedValuesPerTypePerProperty = await preq.get(API.data.propertyValues).then(({ values }) => values) 8 | 9 | export const propertiesWithValuesShortlists = Object.keys(allowedValuesPerTypePerProperty) 10 | 11 | export function getPropertyValuesShortlist ({ type, property }) { 12 | assertString(type) 13 | return allowedValuesPerTypePerProperty[property][pluralize(type)] as EntityUri[] 14 | } 15 | -------------------------------------------------------------------------------- /scripts/sitemaps/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | https://inventaire.io/ 9 | 1 10 | 11 | 12 | https://inventaire.io/signup 13 | 0.9 14 | 15 | 16 | https://inventaire.io/login 17 | 0.9 18 | 19 | 20 | https://inventaire.io/add 21 | 0.9 22 | 23 | -------------------------------------------------------------------------------- /app/modules/search/lib/search_results_history.ts: -------------------------------------------------------------------------------- 1 | import { getLocalStorageStore } from '#app/lib/components/stores/local_storage_stores' 2 | import type { RelativeUrl } from '#server/types/common' 3 | import type { EntityUri, ExtendedEntityType } from '#server/types/entity' 4 | 5 | export const searchResultsHistory = getLocalStorageStore('searches', []) 6 | 7 | export interface HistoryEntry { 8 | uri: EntityUri 9 | label: string 10 | type: ExtendedEntityType 11 | pictures: RelativeUrl[] 12 | timestamp: EpochTimeStamp 13 | } 14 | 15 | export function resortSearchResultsHistory (history) { 16 | return history.sort((a, b) => b.timestamp - a.timestamp) 17 | } 18 | 19 | export function clearSearchHistory () { 20 | searchResultsHistory.set([]) 21 | } 22 | -------------------------------------------------------------------------------- /app/modules/settings/components/user_position_picker.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if showPositionPicker} 14 | showPositionPicker = false}> 15 | showPositionPicker = false} 20 | /> 21 | 22 | {/if} 23 | -------------------------------------------------------------------------------- /app/modules/inventory/lib/importer/parsers/decode_html_entities.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString } from '#app/lib/boolean_tests' 2 | 3 | let element = null 4 | 5 | // Adapted from https://stackoverflow.com/a/9609450/3324977 6 | export default function (str) { 7 | // Ignore this lib in test environments 8 | if (window.document == null) return str 9 | 10 | if (!isNonEmptyString(str)) return str 11 | 12 | if (!element) element = document.createElement('div') 13 | 14 | str = str 15 | // strip script/html tags 16 | .replace(/]*>([\S\s]*?)<\/script>/gmi, '') 17 | .replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, '') 18 | 19 | element.innerHTML = str 20 | str = element.textContent 21 | element.textContent = '' 22 | return str 23 | } 24 | -------------------------------------------------------------------------------- /app/modules/entities/components/lib/edition_action_helpers.ts: -------------------------------------------------------------------------------- 1 | import { property, uniq } from 'underscore' 2 | import { isNonEmptyArray } from '#app/lib/boolean_tests' 3 | import { I18n } from '#user/lib/i18n' 4 | import { mainUser } from '#user/lib/main_user' 5 | 6 | export const getCounterText = editionItems => I18n('users_count_have_this_book', { smart_count: getOwnersCountPerEdition(editionItems) }) 7 | 8 | export const getOwnersCountPerEdition = editionItems => { 9 | if (isNonEmptyArray(editionItems)) { 10 | const notMainUserEditions = editionItems.filter(notMainUserOwner) 11 | const owners = notMainUserEditions.map(property('owner')) 12 | return uniq(owners).length 13 | } 14 | } 15 | 16 | const notMainUserOwner = doc => doc.owner !== mainUser?._id 17 | -------------------------------------------------------------------------------- /app/lib/user_content.ts: -------------------------------------------------------------------------------- 1 | import escape from 'escape-html' 2 | import { formatSpaces } from './utils' 3 | 4 | export const escapeHtml = escape 5 | 6 | // regex inspired by https://gist.github.com/efeminella/2034192 7 | const link = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]+)/gim 8 | const protocolText = '$1' 9 | 10 | export function userContent (text: string) { 11 | if (typeof text === 'string') { 12 | // Escape potential HTML markup to prevent XSS 13 | text = escapeHtml(formatSpaces(text)) 14 | const html = text 15 | .replace(/\n/g, '
') 16 | // Display URLs in the provided text as links 17 | .replace(link, protocolText) 18 | return html 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/modules/users/components/paginated_section_items.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /app/modules/inventory/components/item_show_standalone.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/modules/entities/lib/get_original_lang.ts: -------------------------------------------------------------------------------- 1 | import { pick } from 'underscore' 2 | import wdLang from 'wikidata-lang' 3 | import { pickOne, objLength } from '#app/lib/utils' 4 | import { unprefixify } from '#app/lib/wikimedia/wikidata' 5 | 6 | const langProperties = [ 7 | 'wdt:P103', // native language 8 | 'wdt:P407', // language of work 9 | 'wdt:P1412', // languages spoken, written or signed 10 | 'wdt:P2439', // language (general) 11 | ] 12 | 13 | export default function (claims) { 14 | const langClaims = pick(claims, langProperties) 15 | if (objLength(langClaims) === 0) return 16 | 17 | const originalLangUri = pickOne(langClaims)?.[0] 18 | if (originalLangUri != null) { 19 | const wdId = unprefixify(originalLangUri) 20 | return wdLang.byWdId[wdId]?.code 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/modules/shelves/components/lib/shelves.ts: -------------------------------------------------------------------------------- 1 | import { serializeShelf } from '#shelves/lib/shelves' 2 | import { i18n } from '#user/lib/i18n' 3 | 4 | export const serializeShelfData = (shelf, withoutShelf) => { 5 | let name, description, pathname, title, picture, iconData, iconLabel, isEditable, visibility 6 | 7 | if (withoutShelf) { 8 | name = title = i18n('Items without shelf') 9 | description = '' 10 | pathname = '/shelves/without' 11 | } else { 12 | ;({ name, description } = shelf) 13 | ;({ pathname, picture, iconData, iconLabel, isEditable, visibility } = serializeShelf(shelf)) 14 | title = `${name}${description ? ` - ${description}` : ''}` 15 | } 16 | 17 | return { name, description, pathname, title, picture, iconData, iconLabel, isEditable, visibility } 18 | } 19 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/lib/suggestions/wdt_P195.ts: -------------------------------------------------------------------------------- 1 | import { uniq, pluck } from 'underscore' 2 | import { API } from '#app/api/api' 3 | import preq from '#app/lib/preq' 4 | import { getNonEmptyPropertyClaims } from '#entities/components/editor/lib/editors_helpers' 5 | 6 | export default async function ({ entity }) { 7 | const publishersUris = getNonEmptyPropertyClaims(entity.claims['wdt:P123']) 8 | 9 | if (!publishersUris) return 10 | 11 | const collectionsUris = await Promise.all(publishersUris.map(getPublisherCollections)) 12 | return uniq(collectionsUris.flat()) 13 | } 14 | 15 | const getPublisherCollections = async publishersUris => { 16 | const { collections } = await preq.get(API.entities.publisherPublications(publishersUris)) 17 | return pluck(collections, 'uri') 18 | } 19 | -------------------------------------------------------------------------------- /app/modules/listings/components/listings_layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | {#each listings as listing (listing._id)} 10 | 11 | {/each} 12 | {#if listings.length === 0} 13 |
  • 14 | {i18n('There is nothing here')} 15 |
  • 16 | {/if} 17 |
18 | 19 | 31 | -------------------------------------------------------------------------------- /app/types/importer.d.ts: -------------------------------------------------------------------------------- 1 | import type { getIsbnData } from '#inventory/lib/importer/extract_isbns' 2 | import type { Isbn } from '#server/types/entity' 3 | 4 | interface BaseCandidate { 5 | authors?: string[] 6 | details?: string 7 | editionTitle?: string 8 | goodReadsEditionId?: string 9 | isbnData?: ReturnType 10 | index?: string 11 | libraryThingWorkId?: string 12 | notes?: string 13 | numberOfPages?: number 14 | publicationDate?: string 15 | rawEntry?: unknown 16 | shelvesNames?: string[] 17 | } 18 | 19 | export interface ExternalEntry extends BaseCandidate { 20 | error?: any 21 | isbn?: Isbn 22 | isbnData?: ReturnType 23 | } 24 | 25 | export interface Candidate extends BaseCandidate { 26 | waitingForItemsCount?: Promise 27 | } 28 | -------------------------------------------------------------------------------- /app/modules/general/lib/forms.ts: -------------------------------------------------------------------------------- 1 | import { newError } from '#app/lib/error' 2 | import { I18n } from '#user/lib/i18n' 3 | 4 | export function pass (options) { 5 | const { value, tests, selector } = options 6 | for (const err in tests) { 7 | const test = tests[err] 8 | if (test(value)) throwError(err, selector, value) 9 | } 10 | } 11 | 12 | // format the error to be catched by catchAlert 13 | // ex: throwError 'a title is required', '#titleField' 14 | export function throwError (message, selector, ...context) { 15 | const err = newError(I18n(message)) 16 | err.selector = selector 17 | err.context = context 18 | // Form errors are user's errors, thus they don't need to be reported to the server 19 | // Non-standard convention: 499 = client user error 20 | err.statusCode = 499 21 | throw err 22 | } 23 | -------------------------------------------------------------------------------- /app/modules/entities/components/lib/work_helpers.ts: -------------------------------------------------------------------------------- 1 | import { uniq, omit, flatten, compact } from 'underscore' 2 | import { authorsProps } from '#entities/components/lib/claims_helpers' 3 | 4 | export const getPublishersUrisFromEditions = editions => { 5 | return uniq(compact(flatten(editions.map(edition => { 6 | return findFirstClaimValue(edition, 'wdt:P123') 7 | })))) 8 | } 9 | 10 | export const omitNonInfoboxClaims = claims => { 11 | const omitProps = [ ...authorsProps, 'wdt:P1680' ] 12 | return omitClaims(claims, omitProps) 13 | } 14 | 15 | export const omitClaims = (claims, properties) => { 16 | return omit(claims, properties.flat()) 17 | } 18 | 19 | const findFirstClaimValue = (entity, prop) => { 20 | const values = entity?.claims[prop] 21 | if (!values || !values[0]) return 22 | return values[0] 23 | } 24 | -------------------------------------------------------------------------------- /app/modules/general/lib/confirmation_modal.ts: -------------------------------------------------------------------------------- 1 | import { appLayout } from '#app/init_app_layout' 2 | import { commands } from '#app/radio' 3 | 4 | export interface ConfirmationModalProps { 5 | action: () => Promise | void 6 | confirmationText: string 7 | warningText?: string 8 | formAction?: (formContent: string) => Promise 9 | formLabel?: string 10 | formPlaceholder?: string 11 | yes?: string 12 | no?: string 13 | yesButtonClass?: 'string' 14 | back?: () => void 15 | } 16 | 17 | export async function askConfirmation (options: ConfirmationModalProps) { 18 | const { default: ConfirmationModal } = await import('#general/components/confirmation_modal.svelte') 19 | appLayout.showChildComponent('modal', ConfirmationModal, { 20 | props: options, 21 | }) 22 | commands.execute('modal:open') 23 | } 24 | -------------------------------------------------------------------------------- /app/lib/components/actions/viewport.ts: -------------------------------------------------------------------------------- 1 | // Inspired by https://www.youtube.com/watch?v=1SKKzdHVvcI 2 | // https://svelte.dev/repl/c6a402704224403f96a3db56c2f48dfc 3 | 4 | let intersectionObserver 5 | 6 | function ensureIntersectionObserverExists () { 7 | if (intersectionObserver) return 8 | 9 | intersectionObserver = new IntersectionObserver(entries => { 10 | entries.forEach(entry => { 11 | const eventName = entry.isIntersecting ? 'enterViewport' : 'leaveViewport' 12 | entry.target.dispatchEvent(new CustomEvent(eventName)) 13 | }) 14 | }) 15 | } 16 | 17 | export default function viewport (element) { 18 | ensureIntersectionObserverExists() 19 | 20 | intersectionObserver.observe(element) 21 | 22 | return { 23 | destroy () { 24 | intersectionObserver.unobserve(element) 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/modules/user/lib/email_tests.ts: -------------------------------------------------------------------------------- 1 | import { API } from '#app/api/api' 2 | import { isEmail } from '#app/lib/boolean_tests' 3 | import preq from '#app/lib/preq' 4 | import { pass } from '#general/lib/forms' 5 | import type { Email } from '#server/types/user' 6 | 7 | // Verifies that the email isnt already in use 8 | export function verifyEmailAvailability (email: Email) { 9 | return preq.get(API.auth.emailAvailability(encodeURIComponent(email))) 10 | } 11 | 12 | const emailTests = { 13 | "it doesn't look like an email" (email) { 14 | return !isEmail(email) 15 | }, 16 | } 17 | 18 | export function testEmail (email) { 19 | pass({ 20 | value: email, 21 | tests: emailTests, 22 | }) 23 | } 24 | 25 | export async function verifyEmail (email) { 26 | testEmail(email) 27 | if (email) await verifyEmailAvailability(email) 28 | } 29 | -------------------------------------------------------------------------------- /bundle/webpack.config.prod.cjs: -------------------------------------------------------------------------------- 1 | const webpackCommonConfigFactory = require('./webpack.config.common.cjs') 2 | 3 | const mode = 'production' 4 | const webpackConfig = webpackCommonConfigFactory(mode) 5 | 6 | Object.assign(webpackConfig, { 7 | mode, 8 | devtool: 'source-map', 9 | // Determines the target based on the browserslist set in package.json 10 | // Run `browserslist --update-db` from time to time to update the generated list 11 | target: 'browserslist', 12 | cache: { 13 | type: 'filesystem', 14 | profile: true, 15 | }, 16 | }) 17 | 18 | webpackConfig.output.filename = '[name].[contenthash:8].js' 19 | 20 | if (!process.env.DOCKER) { 21 | webpackConfig.plugins.push(require('./plugins/bundle_analyzer.cjs')) 22 | } 23 | webpackConfig.optimization = require('./optimization.cjs') 24 | 25 | module.exports = webpackConfig 26 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/image_value_display.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /app/modules/general/scss/_links_utils.scss: -------------------------------------------------------------------------------- 1 | @mixin underline($color:#222){ 2 | text-decoration: underline; 3 | text-decoration-color: $color; 4 | } 5 | 6 | @mixin underline-on-hover($color:#222){ 7 | text-decoration: none; 8 | &:hover, &:focus{ 9 | text-decoration: underline; 10 | text-decoration-color: $color; 11 | } 12 | } 13 | 14 | @mixin link-underline-on-hover($color, $hover){ 15 | @include text-hover($color, $hover); 16 | @include underline-on-hover($hover); 17 | } 18 | 19 | @mixin link(){ 20 | @include link-underline-on-hover($link-blue, darken($link-blue, 5%)); 21 | } 22 | 23 | // to be used on dark background 24 | @mixin link-light(){ 25 | @include link-underline-on-hover(white, #eee); 26 | } 27 | 28 | @mixin link-dark(){ 29 | @include link-underline-on-hover($dark-grey, $darker-grey); 30 | } 31 | 32 | $link-hover-grey: lighten($dark-grey, 5%); 33 | -------------------------------------------------------------------------------- /scripts/check_build_environment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cwd=$(pwd) 6 | cd .. 7 | 8 | parent_project=$(node -p "require('./package.json').name") 9 | if [ "$parent_project" != "inventaire" ]; then 10 | echo "can't build: parent directory is not inventaire server directory" 11 | exit 1 12 | fi 13 | 14 | head_rev=$(git rev-parse --short HEAD) 15 | main_rev=$(git rev-parse --short main) 16 | head_branch=$(git symbolic-ref HEAD 2> /dev/null| sed 's|refs/heads/||') 17 | 18 | cd "$cwd" 19 | 20 | if [ "$head_rev" != "$main_rev" ]; then 21 | echo -e "The \e[0;33mserver repository HEAD is not on the 'main' branch\e[0m, but on '${head_branch}' (${head_rev}). 22 | Dev server types might make the client type checks fail. 23 | \e[0;33mBuild the client for production anyway? y/N \e[0m" 24 | read -r response 25 | if [ "$response" != "y" ]; then 26 | exit 1 ; 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /app/modules/entities/components/editor/lib/suggestions/wdt_P123.ts: -------------------------------------------------------------------------------- 1 | import { uniq, flatten } from 'underscore' 2 | import { getNonEmptyPropertyClaims } from '#entities/components/editor/lib/editors_helpers' 3 | import { getReverseClaims, getCollectionsPublishers } from '#entities/lib/entities' 4 | 5 | export default async function ({ entity }) { 6 | const promises = [] 7 | const isbn13h = entity.claims['wdt:P212']?.[0] 8 | 9 | if (isbn13h != null) { 10 | const isbnPublisherPrefix = isbn13h.split('-').slice(0, 3).join('-') 11 | promises.push(getReverseClaims('wdt:P3035', isbnPublisherPrefix)) 12 | } 13 | 14 | const collectionsUris = getNonEmptyPropertyClaims(entity.claims['wdt:P195']) 15 | if (collectionsUris.length > 0) { 16 | promises.push(getCollectionsPublishers(collectionsUris)) 17 | } 18 | 19 | return Promise.all(promises) 20 | .then(flatten) 21 | .then(uniq) 22 | } 23 | -------------------------------------------------------------------------------- /app/types/entity.d.ts: -------------------------------------------------------------------------------- 1 | import type { SerializedEntity, PropertyUri, InvClaimValue, EntityUri, SerializedWdEntity, SerializedRemovedPlaceholder, SerializedInvEntity } from '#server/types/entity' 2 | 3 | // From the client point of view, all entities are server-serialized 4 | // The client can then perform further serialization 5 | export type Entity = SerializedEntity 6 | export type InvEntity = SerializedInvEntity 7 | export type RemovedPlaceholder = SerializedRemovedPlaceholder 8 | export type WdEntity = SerializedWdEntity 9 | 10 | export type EntityDraft = Pick 11 | 12 | export type RedirectionsByUris = Record 13 | 14 | export type Facet = Record 15 | export type Facets = Record 16 | export type FacetsSelectedValues = Record 17 | 18 | export type QueryParams = Record 19 | -------------------------------------------------------------------------------- /scripts/sitemaps/files_commands.js: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import fs from 'node:fs' 3 | import chalk from 'tiny-chalk' 4 | import { folderPath } from './config.js' 5 | 6 | const { grey, green } = chalk 7 | const cp = (orignal, copy) => fs.createReadStream(orignal).pipe(fs.createWriteStream(copy)) 8 | 9 | const { stderr } = process 10 | 11 | export function rmFiles () { 12 | if (folderPath.trim() === '') throw new Error('missing folderPath') 13 | exec(`rm -f ./${folderPath}/*`).stderr.pipe(stderr) 14 | return console.log(grey('removed old files')) 15 | } 16 | 17 | export function generateMainSitemap () { 18 | cp('scripts/sitemaps/main.xml', `./${folderPath}/main.xml`) 19 | return console.log(green('copied main.xml')) 20 | } 21 | 22 | export function mkdirp () { 23 | fs.mkdirSync(`./${folderPath}`, { recursive: true }) 24 | return console.log(grey('created directory')) 25 | } 26 | -------------------------------------------------------------------------------- /app/modules/general/scss/_loader.scss: -------------------------------------------------------------------------------- 1 | // Inspired by https://loading.io/css/ .lds-dual-ring 2 | @mixin loader-commons(){ 3 | display: inline-block; 4 | align-self: center; 5 | &:after{ 6 | content: " "; 7 | display: block; 8 | width: 1em; 9 | height: 1em; 10 | margin: 1px; 11 | border-radius: 50%; 12 | animation: ring-loader 1.2s linear infinite; 13 | } 14 | } 15 | 16 | @keyframes ring-loader{ 17 | 0%{ 18 | transform: rotate(0deg); 19 | } 20 | 100%{ 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | .full-screen-loader{ 26 | @include position(fixed, 0, 0, 0, 0); 27 | @include display-flex(row, center, center); 28 | > div{ 29 | transform: translateY(-50%); 30 | @include loader-commons; 31 | &:after{ 32 | border: 8px solid; 33 | border-color: $dark-grey $light-blue $yellow transparent; 34 | font-size: 5em; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/api/feeds.ts: -------------------------------------------------------------------------------- 1 | import { buildPath } from '#app/lib/location' 2 | import type { AbsoluteUrl } from '#server/types/common' 3 | import type { UserId } from '#server/types/user' 4 | import { mainUser } from '#user/lib/main_user' 5 | import { getEndpointBase } from './endpoint.ts' 6 | 7 | const feedEndpointBase = getEndpointBase('feeds') 8 | // Always using the absolute path so that links are treated as external links, 9 | // thus getting target='_blank' attributes, and the associated click behaviors 10 | // cf app/modules/general/lib/smart_prevent_default.js 11 | const feedEndpoint = `${window.location?.origin}${feedEndpointBase}` as AbsoluteUrl 12 | 13 | export default function (key, id) { 14 | const query: { requester?: UserId, token?: string } = {} 15 | query[key] = id 16 | if (mainUser) { 17 | query.requester = mainUser?._id 18 | query.token = mainUser.readToken 19 | } 20 | 21 | return buildPath(feedEndpoint, query) 22 | } 23 | -------------------------------------------------------------------------------- /app/modules/general/scss/_select.scss: -------------------------------------------------------------------------------- 1 | /* Add height value for select elements to match text input height */ 2 | select{ 3 | width: 100%; 4 | height: $input-height; 5 | appearance: none; 6 | background-color: #fafafa; 7 | background-position: 100% center; 8 | background-repeat: no-repeat; 9 | padding: 0.5rem; 10 | font-size: 0.9rem; 11 | font-weight: normal; 12 | color: $input-font-color; 13 | line-height: normal; 14 | border: 1px solid $input-border-color; 15 | border-radius: 0; 16 | // ForkAwesome is needed to correctly display icons 17 | font-family: 'ForkAwesome', $sans-serif; 18 | &:hover{ 19 | background-color: #f3f3f3; 20 | border-color: #999; 21 | } 22 | &:disabled{ 23 | background-color: #ddd; 24 | cursor: default; 25 | } 26 | option{ 27 | // works in Firefox not Chrome 28 | font-size: 1rem; 29 | padding-block-start: 0.2rem; 30 | padding-block-end: 0.2rem; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/api/users.ts: -------------------------------------------------------------------------------- 1 | import { uniq } from 'underscore' 2 | import type { UserAccountUri } from '#server/types/server' 3 | import Commons from './commons.ts' 4 | import { getEndpointPathBuilders } from './endpoint.ts' 5 | 6 | const { base, action } = getEndpointPathBuilders('users') 7 | 8 | const { 9 | search, 10 | searchByPosition, 11 | } = Commons 12 | 13 | export default { 14 | byIds (ids) { return action('by-ids', { ids: uniq(ids).join('|') }) }, 15 | byUsername (username) { return action('by-usernames', { usernames: username }) }, 16 | byAccts (accts: UserAccountUri[]) { return action('by-accts', { accts: accts.join('|') }) }, 17 | search: search.bind(null, base), 18 | searchByPosition: searchByPosition.bind(null, base), 19 | byCreationDate (params: { limit: number, offset: number, filter?: string }) { 20 | const { limit, offset, filter } = params 21 | return action('by-creation-date', { limit, offset, filter }) 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /app/modules/listings/lib/entities_typing.ts: -------------------------------------------------------------------------------- 1 | import { typesBySection } from '#search/lib/search_sections' 2 | 3 | const { work, serie, author, publisher } = typesBySection.entity 4 | 5 | export const searchTypesByListingType = { 6 | work: [ work, serie ], 7 | author: [ author ], 8 | publisher: [ publisher ], 9 | } 10 | 11 | export const listingTypeByEntitiesTypes = { 12 | work: 'work', 13 | serie: 'work', 14 | human: 'author', 15 | publisher: 'publisher', 16 | } 17 | 18 | export const entitiesTypesByListingType = { 19 | work: [ 'work', 'serie' ], 20 | author: [ 'human' ], 21 | publisher: [ 'publisher' ], 22 | } 23 | 24 | export const i18nTypesKeys = { 25 | work: 'works and series', 26 | author: 'authors', 27 | publisher: 'publishers', 28 | } 29 | 30 | export const i18nSearchPlaceholderKeys = { 31 | work: 'Search for works or series', 32 | author: 'Search for authors', 33 | publisher: 'Search for publishers', 34 | } 35 | -------------------------------------------------------------------------------- /app/types/declarations/svelte.d.ts: -------------------------------------------------------------------------------- 1 | // Addressing TS2345 errors such as: 2 | // Object literal may only specify known properties, and '"on:enterViewport"' does not exist in type 'Omit, never> & HTMLAttributes'.ts(2353) 3 | // See https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/typescript.md#im-getting-deprecation-warnings-for-sveltejsx--i-want-to-migrate-to-the-new-typings 4 | // found via https://github.com/isaacHagoel/svelte-dnd-action/issues/445 5 | 6 | // Reference: https://github.com/sveltejs/svelte/blob/svelte-4/documentation/docs/05-misc/03-typescript.md#enhancing-built-in-dom-types 7 | 8 | declare namespace svelteHTML { 9 | interface HTMLAttributes { 10 | // Define event listners used by actions 11 | 12 | // See app/lib/components/actions/viewport.ts 13 | 'on:enterViewport'?: (e: CustomEvent) => void 14 | 'on:leaveViewport'?: (e: CustomEvent) => void 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/modules/entities/components/cleanup/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 17 |
18 | 19 | 41 | -------------------------------------------------------------------------------- /scripts/sitemaps/generate_index.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { folderPath, index } from './config.js' 3 | import writeSitemap from './write_sitemap.js' 4 | 5 | const exclude = [ index ] 6 | 7 | export function generateIndex () { 8 | const path = `./${folderPath}/${index}` 9 | return writeSitemap(path, generate()) 10 | } 11 | 12 | const generate = () => wrapIndex(getList().map(buildSitemapNode)) 13 | 14 | const getList = () => fs.readdirSync(`./${folderPath}`).filter(file => !exclude.includes(file)) 15 | 16 | const buildSitemapNode = function (filename) { 17 | const url = `https://inventaire.io/${folderPath}/${filename}` 18 | return `${url}` 19 | } 20 | 21 | const wrapIndex = function (sitemapNodes) { 22 | const text = sitemapNodes.join('') 23 | return ` 24 | 25 | ${text} 26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/components/stores/screen.ts: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store' 2 | import { debounce } from 'underscore' 3 | import { waitingForPolyfills } from '#app/init_polyfills' 4 | import { viewportIsSmallerThan, viewportIsLargerThan, getViewportWidth, getViewportHeight } from '#app/lib/screen' 5 | 6 | const getStoreValue = () => { 7 | return { 8 | width: getViewportWidth(), 9 | height: getViewportHeight(), 10 | isSmallerThan: viewportIsSmallerThan, 11 | isLargerThan: viewportIsLargerThan, 12 | } 13 | } 14 | 15 | export const screen = readable(getStoreValue(), set => { 16 | const update = () => set(getStoreValue()) 17 | const lazyUpdate = debounce(update, 100) 18 | window.addEventListener('resize', lazyUpdate) 19 | // Update once window.visualViewport becomes available in legacy browsers 20 | waitingForPolyfills.then(lazyUpdate) 21 | const stop = () => window.removeEventListener('resize', lazyUpdate) 22 | return stop 23 | }) 24 | -------------------------------------------------------------------------------- /app/lib/icons.ts: -------------------------------------------------------------------------------- 1 | import { assertString } from '#app/lib/assert_types' 2 | import barcodeScanner from '#assets/images/barcode-scanner-64.png' 3 | import gutenberg from '#assets/images/gutenberg.png' 4 | import wikidataColored from '#assets/images/wikidata.svg' 5 | import wikisource from '#assets/images/wikisource-64.png' 6 | 7 | const iconAliases = { 8 | giving: 'heart', 9 | lending: 'refresh', 10 | selling: 'money', 11 | inventorying: 'cube', 12 | } 13 | 14 | export function icon (name: string, classes = '') { 15 | assertString(name) 16 | name = iconAliases[name] || name 17 | if (iconPaths[name] != null) { 18 | const src = iconPaths[name] 19 | return `` 20 | } else { 21 | return `` 22 | } 23 | } 24 | 25 | const iconPaths = { 26 | 'barcode-scanner': barcodeScanner, 27 | gutenberg, 28 | 'wikidata-colored': wikidataColored, 29 | wikisource, 30 | } 31 | -------------------------------------------------------------------------------- /app/types/declarations/svelte_context.d.ts: -------------------------------------------------------------------------------- 1 | // Keep separated from `declare namespace svelteHTML` in ./svelte.d.ts 2 | // as, for some reason, `Writable` import messes with `declare namespace svelteHTML` 3 | // (that is, on:enterViewport show type errors again) 4 | import type { PluralizedIndexedEntityType } from '#server/types/entity' 5 | import type { Map, LayerGroup } from 'leaflet' 6 | import type { Writable } from 'svelte/store' 7 | 8 | // Inspired by https://github.com/sveltejs/svelte/issues/8941#issuecomment-1927036924 9 | declare module 'svelte' { 10 | export function getContext(key: 'layer'): (() => LayerGroup) 11 | export function getContext(key: 'map'): (() => Map) 12 | export function getContext(key: 'work-layout:filters-store'): Writable 13 | export function getContext(key: 'layout-context'): string 14 | export function getContext(key: 'search-filter-claim'): string 15 | export function getContext(key: 'search-filter-types'): PluralizedIndexedEntityType[] 16 | } 17 | -------------------------------------------------------------------------------- /app/lib/components/actions/autofocus.ts: -------------------------------------------------------------------------------- 1 | import isMobile from '#app/lib/mobile_check' 2 | 3 | interface AutofocusOptions { 4 | disabled?: boolean 5 | refocusOnVisibilityChange?: boolean 6 | } 7 | 8 | export function autofocus (node, options: AutofocusOptions = {}) { 9 | // Do not auto focus on mobile as it displays the virtual keyboard 10 | // which can take pretty much all the screen 11 | if (isMobile || options.disabled) return 12 | 13 | const { refocusOnVisibilityChange = true } = options 14 | 15 | node.focus() 16 | 17 | function focusOnVisibilityChange () { 18 | if (document.visibilityState === 'visible') node.focus() 19 | } 20 | 21 | if (refocusOnVisibilityChange) { 22 | document.addEventListener('visibilitychange', focusOnVisibilityChange) 23 | } 24 | 25 | return { 26 | destroy () { 27 | if (refocusOnVisibilityChange) { 28 | document.removeEventListener('visibilitychange', focusOnVisibilityChange) 29 | } 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/modules/entities/lib/editor/get_langs_data.ts: -------------------------------------------------------------------------------- 1 | import { uniq } from 'underscore' 2 | import wdLang from 'wikidata-lang' 3 | import { langs as activeLangs } from '#app/lib/active_languages' 4 | import availableLangList from '#app/lib/available_lang_list' 5 | import { getCurrentLang } from '#modules/user/lib/i18n' 6 | import type { WdEntityUri } from '#server/types/entity' 7 | 8 | export function getLangsData (selectedLang, labels) { 9 | const availableLangs = Object.keys(labels) 10 | const highPriorityLangs = [ getCurrentLang(), 'en' ] 11 | const allLangs = uniq(availableLangs.concat(highPriorityLangs, activeLangs)) 12 | // No distinction is made between available langs and others 13 | // as we can't style the