Commit Graph

24 Commits

Author SHA1 Message Date
jordi fita mas 5a16fa44a6 Change media picker from <div> to <dialog> and make it modal
Have to call Dialog.showModal when HTMx loaded the dialog in the DOM,
so had to add a onLoad event listened that checks whether the loaded
element is actually a DIALOG.

Had to restrict the margin: 0 for all elements (*) to exclude dialog,
because the browser sets it to auto, and i did not want to set it again
just because i was too overzealous with my “reset”.

The rest of the CSS is just to have a sticky header and footer, and see
the cancel button, that works as a “close”, all the time.

Finally, i realized that if i add the dialog at the end of the fieldset
and let HTMx inherit its hx-target and hx-swap, i no longer need to set
them in the dialog, as HTMx will always replace the fieldset, and i can
have the dialog side by side the current content of the fieldset, that
it was very confusing seeing it disappear when trying to select a new
media.

The cancel button could now just remove the dialog instead of making the
POST, but in local it makes no difference; we’lls see what happens on
production.
2023-09-22 02:11:03 +02:00
jordi fita mas b5d40cc262 Add the upload form to the media picker
It makes easier to upload new images from the place where we need it,
instead of having to go to the media section each time.

It was a little messy, this one.

First of all, I realized that POSTint to /admin/media/picker to get the
new media field was wrong: i was not asking the server to “accept an
entity”, but only requesting a new HTML value, just like a GET to
/admin/media/upload requests the form to upload a new media, thus here
i should do the same, except i needed the query parameters to change the
field, which is fine—it is actually a different resource, thus a
different URL.

Then, i thought that i could not POST the upload to /admin/media,
because i returned a different HTML —the media field—, so i reused the
recently unused POST to /admin/media/picker to upload that file and
return the HTML for the field.  It was wrong, because i was not
requesting the server to put the file as a subordinate of
/admin/media/picker, only /admin/media, but i did not come up with any
other solution.

Since i had two different upload functions now, i created uploadForm’s
Handle method to refactorize the duplicated logic to a single place.
Unfortunately, i did not work as i expected because uploadForm’s and
mediaPicker’s MustRender methods are different, and mediaPicker has to
embed uploadForm to render the form in the picker.  That made me change
Handle’s output to a boolean and error in order for the HTTP handler
function know when to render the form with the error messages with the
proper MustRender handler.

However, I saw the opportunity of reusing that Handler method for
editMedia, that was doing mostly the same job, but had to call a
different Validate than uploadForm’s, because editMedia does not require
the uploaded file.  That’s when i realized that i could use an interface
and that this interface could be reused not only within media but
throughout the application, and added HandleMultipart in form.

Had to create a different interface for multipart forms because they
need different parameters in Parse that non-multipart form, when i add
that interface, hence had to also change Parse to ParseForm to account
for the difference in signature; not a big deal.

After all that, i realized that i **could** POST to /admin/media in both
cases, because i always return “an HTML entity”, it just happens that
for the media section it is empty with a redirect, and for the picker is
the field.  That made the whole Handle method a bit redundant, but i
left it nevertheless, as i find it slightly easier to read the
uploadMedia function now.
2023-09-22 01:40:22 +02:00
jordi fita mas 97cf117da3 Manage all media uploads in a single place
It made no sense to have a file upload in each form that needs a media,
because to reuse an existing media users would need to upload the exact
same file again; this is very unusual and unfriendly.

A better option is to have a “centralized” media section, where people
can upload files there, and then have a picker to select from there.
Ideally, there would be an upload option in the picker, but i did not
add it yet.

I’ve split the content from the media because i want users to have the
option to update a media, for instance when they need to upload a
reduced or cropped version of the same photo, without an edit they would
need to upload the file as a new media and then update all places where
the old version was used.  And i did not want to trouble people that
uploads the same photo twice: without the separate relation, doing so
would throw a constraint error.

I do not believe there is any security problem to have all companies
link their media to the same file, as they were already readable by
everyone and could upload the data from a different company to their
own; in other words, it is not worse than it was now.
2023-09-21 01:56:44 +02:00
jordi fita mas afe77f2296 Add the services page
This page is more or less similar to home, in terms of database: it
has a carousel and a list of items; in this case, the definition of
campsite services.

As i said early, when adding the home carousel, this carousel has its
own relation and set of functions to manage slides.  They are also
duplicated in Go code, but i think i will need to refactor it later to
a carousel package or something like that, because both relations have
the exact same fields and types, so it makes no sense to have twice the
same code.

I already did it with the CSS and JavaScript code, mostly because it was
easier to replace the `.surroundings div` selector with `.carousel`, and
because that way i can have a single template that loads and initializes
Slick.

There is no UI to create or edit service definitions, although there are
the SQL functions, because i have no more time now, and Oriol needs to
check that the style is correct for that page.
2023-09-17 03:42:16 +02:00
jordi fita mas 8b8dda7969 Add the surroundings static page
This page is “highly stylized”, with a masonry-like grid, that i did not
know how to generate automatically from data defined in PostgreSQL,
therefore with Oriol we agreed to have this one as a static page and
we will see what we can do if the customer asks to be able to change
it.

I was a bit undecided on whether the icons in the bottom part of the
page should be defined in the CSS or with style="" and CSS variables,
like i do for the campsite type in the home page.

At first i thought that it should use CSS variables, mostly for
coherence: if another section of the web does it for its background
image, why no this one.  The difference is that the home page is
dynamically created from the database, while this page is static and we
know what icons we need, thus it makes more sense to move it to the
stylesheet file, because then it will be downloaded by user agents that
actually want to use it (e.g., browsers, but not Braille terminals).
2023-09-17 00:11:39 +02:00
jordi fita mas f746c82b46 Make home page’s carousel manageable via the database
I debated with myself whether to create the home_carousel relation or
rather if it would be better to have a single carousel relation for all
pages.  However, i thought that it would be actually harder to maintain
a single relation because i would need an additional column to tell one
carrousel from another, and what would that column be? An enum? A
foreign key to another relation? home_carousel carries no such issues.

I was starting to duplicate logic all over the packages, such as the
way to encode media paths or “localization” (l10n) input fields.
Therefore, i refactorized them.

In the case of media path, i added a function that accepts rows of
media, because always need the same columns from the row, and it was
yet another repetition if i needed to pass them all the time.  Plus,
these kind of functions can be called as `table.function`, that make
them look like columns from the table; if PostgreSQL implemented virtual
generated columns, i would have used that instead.

I am not sure whether that media_path function can be immutable. An
immutable function is “guaranteed to return the same results given the
same arguments forever”, which would be true if the inputs where the
hash and the original_filename columns, instead of the whole rows, but
i left it as static because i did not know whether PostgreSQL interprets
the “same row but with different values” as a different input.  That is,
whether PostgreSQL’s concept of row is the actual tuple or the space
that has a rowid, irrespective of contents; in the latter case, the
function can not be immutable.  Just to be in the safe side, i left it
stable.

The home page was starting to grow a bit too much inside the app
package, new that it has its own admin handler, and moved it all to a
separate package.
2023-09-15 01:05:38 +02:00
jordi fita mas f48936f800 Add internationalization and localization of campsite types
I am not happy with the localization interface for admins, but it is the
easier that i could think of (for me, i guess), with a separate for
each language.

I am not at all proud of the use of RecordArray, but i did not see the
need to create and register a type just to show the translation links.
I might change my mind when i need to add more and more translation
links, but only it the current interface remains, which i am not that
sure at the moment.
2023-09-12 20:20:23 +02:00
jordi fita mas 7d8cf5439b Fix the vertical alignment of site’s h1 2023-09-12 12:49:46 +02:00
jordi fita mas 2a5751afd5 Show only active campsite types
Relaxed the `max-width: 25%` to fill the whole row in the home page;
it is very unlikely that they will have lees than four types, though.
2023-09-12 12:47:50 +02:00
jordi fita mas 9306acaec3 Add checkbox and style for a mobile “hamburger” menu 2023-09-11 05:43:36 +02:00
jordi fita mas b7e130fed2 Integrate lodges’ and languages’ submenus to the main menu
There is a big difference between the item that has the submenu for
lodges and languages: languages is a link to the “alternate” version of
the page, while the lodges has no page to link to.  Therefore, one is an
anchor while the other is a button, to make a semantic difference, but
both have the exact same appearance here.
2023-09-11 05:13:57 +02:00
jordi fita mas e4053cd844 Change home’s texts to English and add Catalan and Spanish translations 2023-09-11 04:20:21 +02:00
jordi fita mas da127124a1 Add cover media to campsite types
This is the image that is shown at the home page, and maybe other pages
in the future.  We can not use a static file because this image can be
changed by the customer, not us; just like name and description.

I decided to keep the actual media content in the database, but to copy
this file out to the file system the first time it is accessed. This is
because we are going to replicate the database to a public instance that
must show exactly the same image, but the customer will update the image
from the private instance, behind a firewall.  We could also synchronize
the folder where they upload the images, the same way we will replicate,
but i thought that i would make the whole thing a little more brittle:
this way if it can replicate the update of the media, it is impossible
to not have its contents; dumping it to a file is to improve subsequent
requests to the same media.

I use the hex representation of the media’s hash as the URL to the
resource, because PostgreSQL’s base64 is not URL save (i.e., it uses
RFC2045’s charset that includes the forward slash[0]), and i did not
feel necessary write a new function just to slightly reduce the URLs’
length.

Before checking if the file exists, i make sure that the given hash is
an hex string, like i do for UUID, otherwise any other check is going
to fail for sure.  I moved out hex.Valid function from UUID to check for
valid hex values, but the actual hash check is inside app/media because
i doubt it will be used outside that module.

[0]: https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
2023-09-10 03:04:18 +02:00
jordi fita mas ac46759df4 Add media queries to public.css to match original design 2023-09-07 11:47:18 +02:00
jordi fita mas 1f9668104e Add the first test for the front end design
As previously stated, web made the design with an external tool and
had to “convert” it to proper CSS and HTML markup.

Unfortunately, the original design uses slick, that requires jQuery;
i can’t do anything about it now.

Disabled most of the menu and language switcher because it is not in the
design yet.
2023-09-05 04:40:48 +02:00
jordi fita mas 1e1797c1b4 Go back to WYSIWYG for campsite types’ description and remove pages
GrapesJS was not working: too complex for users and not enough for
designers.

Therefore, we decided to use a simple WYSIWYG widget for the campsite
types’ description, while we will do the actual HTML template with an
external editor.  Once that is done, we will convert that HTML to Go
templates and get the description’s content from the database.

Now the pages section has no sense: all the pages will be straight Go
templates.  Only the pages for “special things”, like campsite types,
will use the database, and only for some fields, not the whole page.
2023-08-12 05:41:34 +02:00
jordi fita mas c010b1adba Replace Gutenberg with GrapesJS for pages
I simply can not use Gutenberg without having it choking in its own
over-engineered architecture: using it inside a form, submits it when
clicking the button to change a paragraph’s text size; and using the
custom text size in pixels causes the paragraph component to fail.

The issue with paragraph’s custom text size is that block-editor’s
typography hook expects the font size to be a string, such as '12px' or
'1em', to call startsWith on it, but the paragraph sets an integer,
always assuming that the units are pixels.  Integers do not have a
startsWith method.

Looking at the Gutenberg distributed with the current version of
WordPress, 6.3, seems that now paragraph has a selector for the units,
therefore never sets just the integer.  That made me think that the
components used by the Isolated Block Editor are “mismatched”: maybe in
a previous version of block-editor it was always passed as an integer
too?

I downloaded the source code of the Isolated Block Editor and tried to
update @wordpress/block-library from version 8.14.0 to the current
version, 8.16.0, but fails with an error saying that 'core/paragraph' is
not registered, when, as far as i could check, it was.  Seems that
something changed in @wordpress/blocks between version 12.14.0 and
12.16.0, so i tried to upgrade that module as well; it did not work
because @wordpress/data was not updated —do not remember the actual
error message—.  Upgrading to @wordpress/data from 9.7.0 to 9.9.0 made
the registration of the 'isolated/editor' subregistry to be apparently
ignored, because the posterior select('isolated/editor') within a
withSelect hook returns undefined.

At this point, i gave up: it is obvious that the people that shit
JavaScript for Gutenberg do not care for semantic versioning, and there
are a lot of moving parts to fix just to be able to use a simple
paragraph block!

It seems, however, that there are not many open-source, block-based
_layout_ editors out there: mainly GrapesJS and Craft.JS.  Craft.JS,
however, has no way to output HTML[0], requiring hacks such as using
React to generate the HTML and then pasted that shit onto the page;
totally useless for me.

I am not a fan of GrapesJS either: it seems that the “text block” is
a content-editable div, and semantic HTML can go fuck itself,
apparently.  Typical webshit mentality. By strapping another huge
dependency like CKEditor, but only up to the already out-of-support
version 4, i can write headers, paragraphs and list.  That’s
something, i guess.

[0]: https://github.com/prevwong/craft.js/issues/42

Part of #33.
2023-08-11 02:40:02 +02:00
jordi fita mas e128680e9a Split templates and handlers into admin and public
I need to check that the user is an employee (or admin) in
administration handlers, but i do not want to do it for each handler,
because i am bound to forget it.  Thus, i added the /admin sub-path for
these resources.

The public-facing web is the rest of the resources outside /admin, but
for now there is only home, to test whether it works as expected or not.

The public-facing web can not relay on the user’s language settings, as
the guest user has no way to set that.  I would be happy to just use the
Accept-Language header for that, but apparently Google does not use that
header[0], and they give four alternatives: a country-specific domain,
a subdomain with a generic top-level domain (gTLD), subdirectories with
a gTLD, or URL parameters (e.g., site.com?loc=de).

Of the four, Google does not recommend URL parameters, and the customer
is already using subdirectories with the current site, therefor that’s
what i have chosen.

Google also tells me that it is a very good idea to have links between
localized version of the same resources, either with <link> elements,
Link HTTP response headers, or a sitemap file[1]; they are all
equivalent in the eyes of Google.

I have choosen the Link response headers way, because for that i can
simply “augment” ResponseHeader to automatically add these headers when
the response status is 2xx, otherwise i would need to pass down the
original URL path until it reaches the template.

Even though Camper is supposed to be a “generic”, multi-company
application, i think i will stick to the easiest route and write the
templates for just the “first” customer.

[0]: https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites
[1]: https://developers.google.com/search/docs/specialty/international/localized-versions
2023-08-05 03:42:37 +02:00
jordi fita mas 1b923a9f65 Add add_campsite_type function and call it from Go with a proper form
This form has an “HTML field”, which is just a <textarea> but “improved”
with the use of Automattic’s isolated block editor[0], a repackaged
Gutenberg’s editor playground as full-featured multi-instance editor
that does not require WordPress.

I do not want to use Node to build this huge, over-engineered piece of …
software. Therefore, i downloaded the released “browser” package, and
added the required React bundle, like i do with HTMx.  This will hold
until i need a new custom block type; let’s hope i will not need it.

[0]: https://github.com/Automattic/isolated-block-editor
2023-08-04 19:59:58 +02:00
jordi fita mas f963f54839 Add profile form, inside a user menu
This is the first form that uses HTMx, and can not return a 400 error
code because otherwise HTMx does not render the content.

Since now there are pages that do not render the whole html, with header
and body, i need a different layout for these, and moved the common code
to handle forms and such a new template file that both layouts can use.
I also need the request in template.MustRender to check which layout i
should use.

Closes #7.
2023-07-26 20:46:09 +02:00
jordi fita mas 341520def3 Add favicon
Closes #8.
2023-07-26 18:59:17 +02:00
jordi fita mas cab949f8a6 Show request errors as a toast
Or at least they will once we add the required CSS, but there is at
least the JavaScript behaviour in place now.
2023-07-26 18:52:32 +02:00
jordi fita mas ebe8217862 Add the logout button
Conceptually, to logout we have to “delete the session”, thus the best
HTTP verb would be `DELETE`.  However, there is no way to send a
`DELETE` request with a regular HTML form, and it seems that never will
be[0].

I could use a POST, optionally with a “method override” technique, but
i was planing to use HTMx anyway, so this was as good an opportunity to
include it as any.

In this application i am not concerned with people not having JavaScript
enabled, because it is for a customer that has a known environment, and
we do not have much time anyway.  Therefore, i opted to forgo
progressive enhancement in cases like this: if `DELETE` is needed, use
`hx-delete`.

Unfortunately, i can not use a <form> with a hidden <input> for the
CSRF token, because `DELETE` requests do not have body and the value
should be added as query parameters, like a form with GET method, but
HTMx does the incorrect thing here: sends the values in the request’s
body.  That’s why i have to use a custom header and the `hx-header`
directive to include the CSRF token.

Then, by default HTMx targets the triggered element for swap with the
response from the server, but after a logout i want to redirect the
user to the login form again.  I could set the hx-target to button to
replace the whole body, or tell the client to redirect to the new
location.  I actually do not know which one is “better”.  Maybe the
hx-target is best because then everything is handled by the client, but
in the case of logout, since it is possible that i might want to load
scripts only for logged-in users in the future, i opted for the full
page reload.

However, HTMx does not want to reload a page that return HTTP 401,
hence i had to include the GET method to /login in order to return the
login form with a response of HTTP 200, which also helps when
reloading in the browser after a failed login attempt.  I am not worried
with the HTTP 401 when attempting to load a page as guest, because
this request most probably comes from the browser, not HTMx, and it will
show the login form as intended—even though it is not compliant, since
it does not return the WWW-Authenticate header, but this is the best i
can do given that no cookie-based authentication method has been
accepted[1].

[0]: https://www.w3.org/Bugs/Public/show_bug.cgi?id=10671#c16
[1]: https://datatracker.ietf.org/doc/id/draft-broyer-http-cookie-auth-00.html
2023-07-26 13:49:47 +02:00
jordi fita mas 403c27f1e1 Add the skeleton of the web application
It does nothing more than to server a single page that does nothing
interesting.

This time i do not use a router.  Instead, i am trying out a technique
i have seen in an article[0] that i have tried in other, smaller,
projects and seems to work surprisingly well: it just “cuts off” the
URI path by path, passing the request from handler to handler until
it finds its way to a handler that actually serves the request.

That helps to loosen the coupling between the application and lower
handlers, and makes dependencies explicit, because i need to pass the
locale, company, etc. down instead of storing them in contexts.  Let’s
see if i do not regret it on a later date.

I also made a lot more packages that in Numerus.  In Numerus i actually
only have the single pkg package, and it works, kind of, but i notice
how i name my methods to avoid clashing instead of using packages for
that.  That is, instead of pkg.NewApp i now have app.New.

Initially i thought that Locale should be inside app, but then there was
a circular dependency between app and template.  That is why i created a
separate package, but now i am wondering if template should be inside
app too, but then i would have app.MustRenderTemplate instead of
template.MustRender.

The CSS is the most bare-bones file i could write because i am focusing
in markup right now; Oriol will fill in the file once the application is
working.

[0]: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
2023-07-23 00:11:00 +02:00