diff --git a/front-end/src/components/molecules/add-button.js b/front-end/src/components/molecules/add-button.js index d59884a..39f3595 100644 --- a/front-end/src/components/molecules/add-button.js +++ b/front-end/src/components/molecules/add-button.js @@ -1,7 +1,4 @@ import define from "../../utils/define.js"; -import {globalBus} from "../../utils/events.js"; -import addResult from "./add-result.js"; -import emptyResult from "./empty-result.js"; export default define('add-button', class extends HTMLButtonElement { diff --git a/front-end/src/components/molecules/delete-button.js b/front-end/src/components/molecules/delete-button.js index 1914684..f0097f8 100644 --- a/front-end/src/components/molecules/delete-button.js +++ b/front-end/src/components/molecules/delete-button.js @@ -19,7 +19,7 @@ export default define('delete-button', class extends HTMLButtonElement { const result = this.closest('.result'); const parent = result.parentNode; - const index = Array.prototype.indexOf.call(parent.children, result); + const index = Array.prototype.indexOf.call(parent.getElementsByClassName('result'), result); console.log("Delete index", index); const beginCuratingEvent = new CustomEvent('curate-delete-result', { diff --git a/front-end/src/components/molecules/empty-result.js b/front-end/src/components/molecules/empty-result.js deleted file mode 100644 index 565c52a..0000000 --- a/front-end/src/components/molecules/empty-result.js +++ /dev/null @@ -1,17 +0,0 @@ -import define from '../../utils/define.js'; - -const template = () => /*html*/` -
We could not find anything for your search...
-`; - -export default define('empty-result', class extends HTMLLIElement { - constructor() { - super(); - this.classList.add('empty-result'); - this.__setup(); - } - - __setup() { - this.innerHTML = template(); - } -}, { extends: 'li' }); \ No newline at end of file diff --git a/front-end/src/components/molecules/validate-button.js b/front-end/src/components/molecules/validate-button.js index 65e3b10..997af12 100644 --- a/front-end/src/components/molecules/validate-button.js +++ b/front-end/src/components/molecules/validate-button.js @@ -21,7 +21,7 @@ export default define('validate-button', class extends HTMLButtonElement { const result = this.closest('.result'); const parent = result.parentNode; - const index = Array.prototype.indexOf.call(parent.children, result); + const index = Array.prototype.indexOf.call(parent.getElementsByClassName('result'), result); console.log("Validate index", index); const curationValidateEvent = new CustomEvent('curate-validate-result', { diff --git a/front-end/src/components/organisms/results.js b/front-end/src/components/organisms/results.js index f699f3f..0cbd481 100644 --- a/front-end/src/components/organisms/results.js +++ b/front-end/src/components/organisms/results.js @@ -10,19 +10,12 @@ class ResultsHandler { __setup() { this.__events(); + this.__initializeResults(); } __events() { document.body.addEventListener('htmx:load', e => { - this.results = document.querySelector('.results'); - - // Allow the user to re-order search results - $(".results").sortable({ - "activate": this.__sortableActivate.bind(this), - "deactivate": this.__sortableDeactivate.bind(this), - }); - - this.curating = false; + this.__initializeResults(); }); // Focus first element when coming from the search bar @@ -113,6 +106,18 @@ class ResultsHandler { }); } + __initializeResults() { + this.results = document.querySelector('.results'); + + // Allow the user to re-order search results + $(".results").sortable({ + "activate": this.__sortableActivate.bind(this), + "deactivate": this.__sortableDeactivate.bind(this), + }); + + this.curating = false; + } + __sortableActivate(event, ui) { console.log("Sortable activate", ui); this.__beginCurating(); diff --git a/front-end/src/components/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js deleted file mode 100644 index f700fda..0000000 --- a/front-end/src/components/organisms/search-bar.js +++ /dev/null @@ -1,180 +0,0 @@ -import define from '../../utils/define.js'; -import config from '../../../config.js'; -import { globalBus } from '../../utils/events.js'; -import debounce from '../../utils/debounce.js' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion)').matches; - -const template = () => /*html*/` - -`; - -export default define('search-bar', class extends HTMLElement { - constructor() { - super(); - this.searchInput = null; - this.searchForm = null; - this.abortController = new AbortController(); - this.__setup(); - } - - __setup() { - this.innerHTML = template(); - this.searchInput = this.querySelector('input'); - this.searchForm = this.querySelector('form'); - this.__events(); - } - - __dispatchSearch({ results = null, error = null }) { - const searchEvent = new CustomEvent('search', { - detail: { - results, - error, - }, - }); - globalBus.dispatch(searchEvent) - } - - /** - * Updates the overall layout of the page. - * - * `home` centers the search bar on the page. - * `compact` raises it to the top and makes room for displaying results. - * - * @param {'compact' | 'home'} mode - * @return {void} - */ - __setDisplayMode(mode) { - switch (mode) { - case 'compact': { - document.body.style.paddingTop = '25px'; - document.querySelector('.search-menu').classList.add('compact'); - break; - } - case 'home': { - document.body.style.paddingTop = '30vh'; - document.querySelector('.search-menu').classList.remove('compact'); - break; - } - } - } - - async __executeSearch() { - this.abortController.abort(); - this.abortController = new AbortController(); - // Get response from API - const response = await fetch(`${config.publicApiURL}search?s=${encodeURIComponent(this.searchInput.value)}`, { - signal: this.abortController.signal - }); - // Getting results from API - const search = await (response).json(); - return search; - } - - __handleSearch = async () => { - // Update page title - document.title = `MWMBL - ${this.searchInput.value || "Search"}`; - - // Update query params - const queryParams = new URLSearchParams(document.location.search); - // Sets query param if search value is not empty - if (this.searchInput.value) queryParams.set(config.searchQueryParam, this.searchInput.value); - else queryParams.delete(config.searchQueryParam); - // New URL with query params - const newURL = - document.location.protocol - + "//" - + document.location.host - + document.location.pathname - + (this.searchInput.value ? '?' : '') - + queryParams.toString(); - // Replace history state - window.history.replaceState({ path: newURL }, '', newURL); - - if (this.searchInput.value) { - this.__setDisplayMode('compact') - - try { - const search = await this.__executeSearch() - // This is a guess at an explanation - // Check the searcInput.value before setting the results to prevent - // race condition where the user has cleared the search input after - // submitting an original search but before the search results have - // come back from the API - this.__dispatchSearch({ results: this.searchInput.value ? search : null }); - } - catch(error) { - this.__dispatchSearch({ error }) - } - } - else { - this.__setDisplayMode('home') - this.__dispatchSearch({ results: null }); - } - } - - __events() { - /** - * Always add the submit event, it makes things feel faster if - * someone does not prefer reduced motion and reflexively hits - * return once they've finished typing. - */ - this.searchForm.addEventListener('submit', (e) => { - e.preventDefault(); - this.__handleSearch(e); - }); - - /** - * Only add the "real time" search behavior when the client does - * not prefer reduced motion; this prevents the page from changing - * while the user is still typing their query. - */ - if (!prefersReducedMotion) { - this.searchInput.addEventListener('input', debounce(this.__handleSearch, 500)) - } - - // Focus search bar when pressing `ctrl + k` or `/` - document.addEventListener('keydown', (e) => { - if ((e.key === 'k' && e.ctrlKey) || e.key === '/' || e.key === 'Escape') { - e.preventDefault(); - this.searchInput.focus(); - } - }); - - // Focus first result when pressing down arrow - this.addEventListener('keydown', (e) => { - if (e.key === 'ArrowDown' && this.searchInput.value) { - e.preventDefault(); - const focusResultEvent = new CustomEvent('focus-result'); - globalBus.dispatch(focusResultEvent); - } - }); - - globalBus.on('focus-search', (e) => { - this.searchInput.focus(); - }); - } - - connectedCallback() { - // Focus search input when component is connected - this.searchInput.focus(); - - const searchQuery = new URLSearchParams(document.location.search).get(config.searchQueryParam); - this.searchInput.value = searchQuery; - /** - * Trigger search handling to coordinate the value pulled from the query string - * across the rest of the UI and to actually retrieve the results if the search - * value is now non-empty. - */ - this.__handleSearch(); - } -}); diff --git a/front-end/src/index.js b/front-end/src/index.js index 04c3087..e982c79 100644 --- a/front-end/src/index.js +++ b/front-end/src/index.js @@ -5,6 +5,7 @@ * Please do not pollute this file if you can make * util or component files instead. */ +import 'vite/modulepreload-polyfill'; // Waiting for top-level await to be better supported. (async () => { @@ -17,5 +18,10 @@ import("./components/organisms/results.js"); import("./components/organisms/footer.js"); import("./components/organisms/save.js"); + import("./components/molecules/add-button.js"); + import("./components/molecules/add-result.js"); + import("./components/molecules/delete-button.js"); + import("./components/molecules/result.js"); + import("./components/molecules/validate-button.js"); } })(); diff --git a/front-end/vite.config.js b/front-end/vite.config.js index 4ebb214..5914005 100644 --- a/front-end/vite.config.js +++ b/front-end/vite.config.js @@ -7,12 +7,14 @@ export default { publicDir: '../assets', build: { outDir: '../dist', + manifest: true, rollupOptions: { input: { index: resolve(__dirname, 'src/index.js'), stats: resolve(__dirname, 'src/stats/index.html'), }, }, + minify: false, }, plugins: [ legacy({ diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py index e75ac86..99201f3 100644 --- a/mwmbl/settings_common.py +++ b/mwmbl/settings_common.py @@ -30,7 +30,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'mwmbl', - "django_htmx", + 'django_htmx', + 'django_vite', 'allauth', 'allauth.account', 'allauth.socialaccount', @@ -106,6 +107,9 @@ USE_TZ = True STATIC_URL = 'static/' +DJANGO_VITE_DEV_MODE = False + + # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field diff --git a/mwmbl/settings_dev.py b/mwmbl/settings_dev.py index de11849..344e1bd 100644 --- a/mwmbl/settings_dev.py +++ b/mwmbl/settings_dev.py @@ -13,7 +13,11 @@ DATABASES = { } -STATICFILES_DIRS = [str(Path(__file__).parent.parent / "front-end" / "dist")] +STATIC_ROOT = "" +DJANGO_VITE_ASSETS_PATH = Path(__file__).parent.parent / "front-end" / "dist" +DJANGO_VITE_MANIFEST_PATH = DJANGO_VITE_ASSETS_PATH / "manifest.json" + +STATICFILES_DIRS = [str(DJANGO_VITE_ASSETS_PATH)] DEBUG = True diff --git a/mwmbl/settings_prod.py b/mwmbl/settings_prod.py index 837dbb0..7716636 100644 --- a/mwmbl/settings_prod.py +++ b/mwmbl/settings_prod.py @@ -9,8 +9,9 @@ SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] STATIC_ROOT = "/app/static/" -STATICFILES_DIRS = ["/front-end-build/"] +DJANGO_VITE_ASSETS_PATH = "/front-end-build/" +STATICFILES_DIRS = [DJANGO_VITE_ASSETS_PATH] DATABASES = {'default': dj_database_url.config(default=os.environ["DATABASE_URL"])} diff --git a/mwmbl/templates/index.html b/mwmbl/templates/index.html index a9e7d08..957c60d 100644 --- a/mwmbl/templates/index.html +++ b/mwmbl/templates/index.html @@ -1,3 +1,4 @@ +{% load django_vite %} @@ -45,6 +46,9 @@ + + + {% vite_hmr_client %} @@ -53,9 +57,7 @@