Customer does not want a contact page, but a page where they can write
the direction on how to reach the campground, with a Google map embed
instead of using Leaflet, because Google Maps shows the reviews right
in the map.
That means i had to replace the GPS locations with XML fields for the
customer to write. In all four languages.
This time i tried a translation approach inspired by PrestaShop: instead
of opening a new page for each language, i have all languages in the
same page and use AlpineJS to show just a single language. It is far
easier to write the translations, even though you do not have the source
text visible, specially in this section that there is no place for me
to put the language links.
Customer does not want the next slide to show partially; either it shows
theo whole slide, or not at all.
Had to remove the min-width for campsite type’s spiel, or it would make
the whole thing fall over, i do not know why; possibly because slick
could not reduce the width to its expected value.
I use Sortable, exactly like HTMx’s sorting example does[0]. Had to
export the slug or ID of some entries to be able to add it in the hidden
input.
For forms that use ID instead of slug, had to use an input name other
than “id” because otherwise the swap would fail due to bug #1496[1]. It
is apparently fixed in a recent version of HTMx, but i did not want to
update for fear of behaviour changes.
[0]: https://htmx.org/examples/sortable/
[1]: https://github.com/bigskysoftware/htmx/issues/1496
Had to change setup_redsys because admins can not read the current
encrypt key, thus it is not possible to `set encrypt_key =
coalesce(…, encrypt_key)`.
Not that it did much sense, anyway, as i was already inside the branch
of the if when encrpty_key is null.
However, it seems that this also affects in the `on conflict` update. I
assume this is because `excluded` is some kind of row of the relation
and has the same restrictions.
The idea is that the booking form will be prefilled with the values
passed from that other mini-form, and the campsite type is implicit
due to the page where the form is located at, but i need to give it to
the booking page.
The booking page does not yet use that information.
The form is based on the one in the current website, but in a single
page instead of split into many pages; possibly each <fieldset> should
be in a separate page/view. The idea is for Oriol to check the design
and decide how it would be presented to the user, so i needed something
to show him first.
I hardcoded the **test** data for the customer’s Redsys account. Is
this bad? I hope not, but i am not really, really sure.
The data sent to Redsys is just a placeholder because there are booking
details that i do not know, like what i have to do with the “teenagers”
field or the area preferences, thus i can not yet have a booking
relation. Nevertheless, had to generate a random order number up to
12-chars in length or Redsys would refuse the payment, claiming that
the order is duplicated.
The Redsys package is based on the PHP code provided by Redsys
themselves, plus some hints at the implementations from various Go
packages that did not know why they were so complicated.
Had to grant select on table country to guest in order to show the
select with the country options.
I have changed the “Postal code” input in taxDetails for “Postcode”
because this is the spell that it is used in the current web, i did not
see a reason to change it—it is an accepted form—, and i did not want to
have inconsistencies between forms.
Oriol does not want to waste so much vertical space for the calendar,
and wants it to show in a carousel, initially with only 6 months, and
loading the next three each time the user scrolls past the last.
I now use HTMx in the public page too for this auto-loading behavior,
based on their “infinite scroll” example[0].
Had to put the /calendar URI inside campsites because in the
calendar.gohtml i do not know the current type’s UUID, and can not use
a relative URL to “add subdirectories”, because the type does not end
with a slash.
Had to change season.CollectCalendar to expect the first month and a
number of months to show, to be able to load only 6 or 3 months after
the current, for the initial carousel content, or after the last month
of the carousel.
[0]: https://htmx.org/examples/infinite-scroll/
I had to export the Calendar type from Season to use it from
campsite/types, and also renamed them because season.SeasonCalendar is
a bit redundant compared to just season.Calendar.
I still have not added the HTMx code to switch year because i am not
sure whether Oriol will want to show a whole year or just half a year.
The calculation for the text color taking into account the contrast with
the background is from [0].
[0]: https://www.smashingmagazine.com/2020/07/css-techniques-legibility/#foreground-contrast
In the old website, the prices where show with all the options, but in
the new design only a single price is show, that in the case of
campsites with options is the price per night of the “base” plus the
minimum options selected.
This is the text that introduces the carousel; it is not a spiel, but
this is what i call it.
It turns out that this text needs to have paragraphs and headings, much
like home’s slider, rather than the one in services page, thus no need
to change its font size or to align all items in the carousel in the
middle.
I can not reuse the carousel package because these carousels need the
campsite site’s slug as a first parameters: i can not have a relation
per campsite type, as i do in home and services pages, because the
campsite types are added by administration types; even if i had a
single relation for slides of home and services pages, these would go
in a different relation due to the foreign key to campsite type.
What i could reuse, however, is the Slide and SlideEntry types from
that package, although i had to export carousel.Translation to be usable
from the types package. I should change that to use locale.Translation,
but this was the easier option, or i would need to change the queries
and templates for carousel package too.
Besides that, they work exactly like the slides in home and services
pages.
I created a common template to show the company address in the footer
and the contact page, and then i realized Go did not like to output my
phone URL in the anchor without having the tel: schema in the template.
I then removed that variable and now the URL is created with tel: and
the phone number with its spaces removed.
I was not sure whether to use PostGIS to store the GPS location of the
company, as i am sure i will only use that point just to show the map.
However, just in case, it is not a big deal.
There is no way to change that from the administration pages for now,
because of time constraints, and it is very unlikely that they will
change the campgrounds’ location in the near future.
The location is in a separate table because i did not want to have to
change every test file, to be honest, but this also makes the map
“optional” without the need for NULL values.
I added the contact address to every public page because the new design
adds it to the footer, so i will be needing it everywhere, just like the
menu.
This calendar is supposed to be edited by admin users, but do not yet
have the complete JavaScript code to do so, thus for now i have made it
read-only.
I do not like confirmation messages: they question user’s actions, and
in general it is better to offer an undo option afterward. However, undo
is harder to implement, and currently i do not have time to do this.
The delete for the session is different because the only repercussion
would be to log in again; the user is not in danger of losing any data
whatsoever.
This one has an input to select the icon. It makes no sense to choose
an icon only by name, thus a <select> is not appropriate, and had to
use a hidden input with a row of button to choose the icon from. This
works now only because there are very few icons; we’ll need to choose
a different approach when there are many more icons.
Since now the icons have to be defined in CSS for both the public and
admin sections, i had to split it into a separate file that both sites
can use. I considered the option to “include” that CSS with m4, like
i do for images in demo.sql, but it made everything too complicated
(e.g., having to call make for each change in the CSS), and decided to
load that CSS in a separate <link>.
We discussed with Oriol how to show these “extra” menu items, as they
can’t be in the horizontal menu we have intended for employees, because
there is not enough horizontal space.
Oriol suggested to move these into the user menu. In fact, the company
settings was already there, which means that i already wanted to do that
from the very beginning, i believe, but i must have forgotten it along
the way…. Or maybe it was because this is where Numerus has the company
settings menu item, too, and i did not see the relation with the rest;
i do not know.
I decided to use a custom attribute for the campsite label in the SVG
because i was a bit wary of reusing ‘id’ for that, specially given that
most labels are number only and XML can not have IDs starting with a
number.
In fact, at least Inkscape and Affinity solve the problem by having an
additional foreign attribute to keep the “group label” in without that
restriction (‘inkscape:label’ and ‘serif:id’, respectively), thus i
thought of doing the same, but with a namespace that i control and be
independent of the design program.
However, it seems that Affinity does not have a way of editing the XML
attributes like Inkscape does[0], thus there is no way of adding or
editing that value from there; i can not ask Oriol to edit the SVG file
in a text editor each time.
We have agreed to reuse the ‘id’ attribute to contain the campsite’s
label by using a specific prefix, that we checked is editable without
issue in Affinity’s UI.
[0]: https://forum.affinity.serif.com/index.php?/topic/24318-xml-data/&do=findComment&comment=115609
I am using the US terms for campground and campsite, that’s why the
relation is called ‘campsite’ instead of ‘pitch’, but i used the wrong
terminology in the SVG map because the customer uses the UK term and
call themselves campsite, so i mixed things.
It is now the campground map and each individual area is a campsite,
as i have been using all along.
I did not remember that the file was loaded as a module, so i have
to export it in order to use from the <script> in media. That <script>
needs to be also a module and explicitly import the function; since it
is already loaded, the browser does not load the file again.
This link is supposed to be for employees, to see the map and check
on campsites’ availability. Currently, it shows the same for employees
and admins, but it will need to change.
I intend to use the same SVG file for customers and employees, so i had
to change Oriol’s design to add a class to layers that are supposed to
be only for customers, like trees. These are hidden in the admin area.
I understood that customers and employees have to click on a campsite to
select it, and then they can book or whatever they need to do to them.
Since customers and employees most certainly will need to have different
listeners on campsites, i decided to add the link with JavaScript. To
do so, i need a custom XML attribute with the campsite’s identifier.
Since i have seen that all campsites have a label, i changed the
“identifier” to the unique combination (company_id, label). The
company_id is there because different companies could have the same
label; i left the campsite_id primary key for foreign constraints.
In this case, as a test, i add an <a> element to the campsite with a
link to edit it; we’ll discuss with Oriol what exactly it needs to do.
However, the original design had the labels in a different layer, that
interfered with the link, as the numbers must be above the path and
the link must wrap the path in order to “inherit” its shape. I had no
other recourse than to move the labels in the same layer as the paths’.
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.
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.
This is required if requesting an admin page via HTMx; i do not think it
is done as of now, except for the media picker, that has no layout
because i do not want to update the title.
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.
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.
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).
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.
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.
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.
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
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.
Seasons have a color to show on the calendar. I need them in HTML format
(e.g., #123abc) in order to set as value to `<input type="color">`, but
i did not want to save them as text in the database, as integers are
better representations of colors—in fact, that’s what the HTML syntax
also is: an integer.
I think the best would be to create an extension that adds an HTML color
type, with functions to convert from many representations (e.g., CSS’
rgb or even color names) to integer and back. However, that’s a lot of
work and i can satisfy Camper’s needs with just a couple of functions
and a domain.
To show the color on the index, at first tried to use a read-only
`<input type="color">`, but seems that this type of input can not be
read-only and must be disabled instead. However, i do not know whether
it makes sense to have a disabled input outside a form “just” to show
a color; i suspect it does not. Thus, at the end i use SVG with a
single circle, which is better that a 50%-rounded div with a background
color, even if the result is the same—SVG **is** intended for showing
pictures, which is this case.
I realized that tax details, campsite types, and campsites pages are all
part of the settings of the company, in the sense that all of them are
set up by a company administrator, and should be under the same item in
the user menu.
The template for these tabs is in the same layout.gohtml file because
i did not want to repeat the tabs everywhere it were used, or i would
forget some of them when adding new tabs, and did not want to add a new
file just for that.
It is inside the “user menu” only because this is where Numerus has the
same option, although it makes less sense in this case, because Numerus
is geared toward individual freelancers while Camper is for companies.
But, since it is easy to change afterward, this will do for now.
However, it should be only shown to admin users, because regular
employees have no UPDATE privilege on the company relation. Thus, the
need for a new template function to check if the user is admin.
Part of #17.
For now, there is only the label, type, and active fields. We will need
some field to hold the area on the map, but this requires #4, and
possibly #6, to be finished.
Part of #27.
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.
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.
For now, this is almost identical to the campsite types, but this
section is for purely informational pages that have no other relation
to the database than “belongs to the same company”.
Part of #33.
Had to export and move PublicPage struct to template because i can not
import app from campsites/types: app already imports campsite for the
http handler, and it, in turn, imports the types package for its own
http handler; an import loop.
Also had to replace PublicPage.MustRender with a Setup function because
the page passed down to html/template was the PublicPage struct, not
whatever struct embeds it. I was thinking more of Java inheritance here
rather than struct embedding.
The language switcher needs the same information as languageLinks
needed, namely the list of locales and the current Path, to construct
the URI to all alternate versions. However, in this case i need access
to this data in the template context, to build the list of links.
At first i use request’s context to hold the list of available locales
from application, and it worked, possibly without ill-effects, but i
realized that i was doing it just to avoid a new parameter. Or, more
precise, an _explicit_ parameter; the context was used to skip the
inner functions between app and template.MustRenderPublic, but the
parameter was there all the same.
Finally, i thought that some handler might want to filter the list of
locales to show only the ones that it has a translation of. In that
case, i would need to extract the locales from the context, filter it,
and create a new request with the updated context. That made little
sense, and made me add the explicit locales parameter.
Since now the template has the same data as languageLinks, there is
little point of having the link in the HTTP response headers, and added
the <link> elements to <head>.
I thought that maybe i could avoid these <links> as they give the exact
same data as the language switch, but Google says nothing of using
regular anchors to gather information about localized versions of the
document[0], thus i opted to be conservative. One can reason that the
<head> has more weight for Google, as most sites with user-generated
content, which could contain these anchors, rarely allow users to edit
the <head>.
[0]: https://developers.google.com/search/docs/specialty/international/localized-versions
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
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
We made the decision that this application will also serve the public
pages to guests and customers, to avoid the overhead of having to
synchronize all data between this application and a bespoke WordPress
plugin.
That means that i no longer can have a /company/slug in the URL to know
which company the request is for, not only because it looks ugly but
because guest users do not have a “main company”—or any company
whatsoever.
Since the public-facing web is going to be served through a valid DNS
domain, and all companies are going to have a different domain, i
realized this is enough: i only had to add a relation of company and
their hosts. The same company can have many hosts for staging servers
or to separate the administration and public parts, for instance.
With change, the company is already known from the first handler, and
can pass it down to all the others, not only the handlers under
/company/slug/whatever. And i no longer need the companyURL function,
as there is no more explicit company in the URL.
Even though template technically does not need the template, as it only
contains the ID —the rest of the data is in a relation inaccessible to
guests for now—, but i left the parameter just in case later on i need
the decimal digits or currency symbol for whatever reason.
I really doubt that they are going to use more than a single company,
but the application is based on Numerus, that **does** have multiple
company, and followed the same architecture and philosophy: use the URL
to choose the company to manage, even if the user has a single company.
The reason i use the slug instead of the ID is because i do not want to
make the ID public in case the application is really used by employees
of many unrelated companies: they need not need to guess how many
companies there are based on the ID.
I validate this slug to be a valid UUID instead of relaying on the
query’s empty result because casting a string with a malformed value to
UUID results in an error other than data not found. Not with that
select, but it would fail with a function parameter, and i want to add
that UUID check to all functions that do use slugs.
I based uuid.Valid function on Parse() from Google’s uuid package[0]
instead of using regular expression, as it was my first idea, because
that function is an order of magnitude faster in benchmarks:
goos: linux
goarch: amd64
pkg: dev.tandem.ws/tandem/numerus/pkg
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkValidUuid-4 36946050 29.37 ns/op
BenchmarkValidUuid_Re-4 3633169 306.70 ns/op
The regular expression used for the benchmark was:
var re = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
And the input parameter for both functions was the following valid UUID,
because most of the time the passed UUID will be valid:
"f47ac10b-58cc-0372-8567-0e02b2c3d479"
I did not use the uuid package as is, even though it is in Debian’s
repository, because i only need to check whether the value is valid,
not convert it to a byte array. As far as i know, that package can not
do that.
Adding the Company struct into auth was not my intention, as it makes
little sense name-wise, but i need to have the Company when rendering
templates and the company package has templates to render, thus using
the company package for the Company struct would create a dependency
loop between template and company. I’ve chosen the auth package only
because User is also there; User and Company are very much related in
this application, but not enough to include the company inside the user,
or vice versa, as the User comes from the cookie while the company from
the URL.
Finally, had to move methodNotAllowed to the http package, as an
exported function, because it is used now from other packages, namely
campsite.
[0]: https://github.com/google/uuid
It does nothing, but i need it to discuss with Oriol.
Now there are more than a single package that requires shiftPath, so
i moved it to http and an exported function, ShiftPath.
Part of #25.
I do not see the profile image as an “integral part” of the user (i.e.,
no need for constraints), hence i do not want to store it in the
database, as would do for the identification image during check-in.
By default, i store the avatars in /var/lib/camper/avatars, but it is a
variable to allow packagers change this value using the linker.
This is also served as a test bed for uploading files to the server,
that now has a better interface and uses less resources that what i did
to Numerus.
Now the profile handler needs to keep a variable to know the path to the
avatars’ directory, thus i had to change it to a struct nested in app,
much like the fileHandler does. It still has to return the HandlerFunc,
however, as this function needs to close over the user and connection
variables.
Part of #7.
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.
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
To get the user from the database i have to set the cookie first, that
was already done in database.MustAcquire, but i thought they were too
far apart, even thought they are so related. So, the cookie, and thus
the role, is set when getting the user, that is actually the first thing
to do once the connection is acquired. However, that way the database
package has no knowledge of cookies, and the code that sets the cookie
and retrieves the user are next to each other.
I applied the same logic to the changes of locale.Match: it has not
business knowing that the accept language string comes from a request;
it only needs the actual string. Also, the TODO comment about getting
the user’s locale made no sense, now, because app already knows that
locale, so there is no need to pass the user to the locale package.
Getting the locale is done after retrieving the user from the database,
for the same reason the connection is Acquired as far up as possible:
almost every request will need this value, together with the user and
the database connection.
I am a bit affraid that i will end up with functions that always expect
these three values. Maybe i can put the locale inside user, as it is
the user’s locale, after all, no matter if it came from the database or
the user agent, but connection and user must be separate, i think.
We’ll see.
It is a lot of code having to check the login variables inside the POST
handler, and i could not mark each input field individually as invalid
because the generic errors array i was using did no identify which field
had the error.
Thus, i use more or less the same technique as with Numerus: a struct
with the value and the error message. This time the input field does
not have the label and extra attributes because i believe this belongs
to the template: if i want do reuse the same form template, i should
create a common template rather than defining everything in Go.
The name is a bit different, however, because it has meaning both to the
front and back ends, as it needs to be exactly the same. Writing it
twice is error-prone, as with a rename i could easily forget to change
one or the other, and here i see value in having that in Go, because
it is also used in code.
I now actually handle the /login URL and check whether the email and
password are valid, creating the session cookie if correct, but doing
nothing else with that cookie, for now.
The validation is done by hand for now, because i do not yet how i will
actually do it without so much duplication.
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/