Commit Graph

136 Commits

Author SHA1 Message Date
jordi fita mas 819c0412d2 Add missing campsites and mark them on the SVG map 2023-09-25 12:51:11 +02:00
jordi fita mas bba555be10 Replace SVG’s foreign attribute with a specific prefix for IDs
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
2023-09-25 12:44:47 +02:00
jordi fita mas 6d84b8baad Fix campground and campsite nomenclature for SVG map
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.
2023-09-25 12:34:05 +02:00
jordi fita mas 233aacc2de Export camperUploadForm function
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.
2023-09-24 03:19:46 +02:00
jordi fita mas 4aa74c0768 Add the “normal” link to campsite administration
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.
2023-09-24 03:18:39 +02:00
jordi fita mas e3503187d3 Add campsite map in SVG
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’.
2023-09-24 03:17:13 +02:00
jordi fita mas 8c0dbf7806 Fix the label and prompt for the carousel media field 2023-09-22 02:17:56 +02:00
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 7e748bcaa5 Add empty settings-tabs to htmx layout
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.
2023-09-21 01:58:29 +02:00
jordi fita mas e1d0bbb3ee Fix the page title for the Services Page’ index template 2023-09-21 01:57:14 +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 d08fa31c81 Add translate_campsite_type SQL function
It makes sense that, if i have it for the home page sliders, i should
also have it for the campsite type, for consistence, at least.
2023-09-15 01:23:51 +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 7330ae83ec Copy RecordArray from pgtype to database
Debian’s pgtype version is 1.10.0, but pgtype did not add RecordArray
type until 1.11.0, thus could not compile the application with
Debian 12.

I just copied the code from 1.11.0 without any modification besides
adding the reference to the pgtype package to types and functions.
2023-09-12 21:58:30 +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 75aa8900b6 Add English as an available, selectable language 2023-09-12 12:27:00 +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 6f07de3230 Add the original filename to media’s URL
It server no other purpose than to “look better” for humans.
2023-09-11 03:31:27 +02:00
jordi fita mas 151f7fc84e Add the sample campsite types to the demo file
Since campsite types need a media, i have to insert also images to the
media relation.  The best would be to use PostgreSQL’s
pg_read_binary_file to read the media content from actual files when
inserting the new rows, but the files need to be within the database
cluster directory, or have to use an absolute path when running as a
superuser to read from files outside the cluster directory, which means
that it would depend on the path where i leave the files, that is
different in development that in staging.

To avoid that problem i can simply insert the rows using their base64
strings, with PostgreSQL’s decode.  The images are kind of small, but
i was worried that each change in demo.sql would duplicate that data in
git, even if the change is not related to the images, because git stores
the whole file; even if small, soon everything adds up.

I do not care if the _final_ demo.sql is big, as this file is packaged
in a different deb and is only installed in staging, so i’ve chosen to
use m4 to build a single “amalgamated” SQL file from the base .sql
file and the individual image files converted to base64 strings.  That
way, each image is individually managed by git and the base .sql file
does not balloon up for each little change.

Changed m4’s quotes to [[ ]] because the default ` ' was interfering
with Intellij’s syntax highlighting.
2023-09-10 03:57:46 +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 de0fac1368 Add media relation and add_media function 2023-09-08 20:03:26 +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 b4919db6c4 Add seasons’ relation, functions, and admin section
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.
2023-08-16 20:15:57 +02:00
jordi fita mas 1837b7a113 Fix pgettext context of campsite’s active checkbox 2023-08-16 20:06:49 +02:00
jordi fita mas e0021ae994 Fix the description of an add_campsite’s test case 2023-08-16 20:05:40 +02:00
jordi fita mas 000a2e506e Add tabs to “company settings” pages to link each other
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.
2023-08-16 10:42:05 +02:00
jordi fita mas 50fbfce9ee Add the form to update company’s tax details
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.
2023-08-15 22:35:21 +02:00
jordi fita mas 93364b896c Remove the menu aria role from admin’s layout
Elements with the menu role require complex functionality and a keyboard
navigation[0] that i do not implement, thus the role is incorrect and
more harmful than anything else.

[0]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menu_role
2023-08-15 20:52:14 +02:00
jordi fita mas 98880d9173 Test that new campsite types created by PL/pgSQL function are active 2023-08-14 20:19:17 +02:00
jordi fita mas 216ae20638 Add the campsite relation, HTTP handlers, and form
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.
2023-08-14 20:18:26 +02:00
jordi fita mas 8b5a45299e Return HTTP 404 response for non-active campsite’s public pages
At first i thought i had to return HTTP 410 gone in this case, but
HTTP Semantics RFC[0] says that “The 410 (Gone) status code indicates
that […] this condition is likely to be permanent. If the origin server
does not know […] whether or not the condition is permanent, the status
code 404 (Not Found) ought to be used instead.”

A non-active campsite type does not mean “deleted”, but rather
temporarily disabled, thus a 404 is the appropriate code.

[0] https://www.rfc-editor.org/rfc/rfc9110#status.410
2023-08-14 12:07:51 +02:00
jordi fita mas 114c6e6212 Order campsite type’s index by name 2023-08-14 11:46:57 +02:00
jordi fita mas 208952b964 Add the Active field to the campsite type’s edit form and function
In the new form this field is hidden and always active, because it makes
no sense to add an inactive campsite type.
2023-08-14 11:43:58 +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 c0f532df4e Add the pages section
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.
2023-08-08 20:09:57 +02:00
jordi fita mas d6d2a9b843 Add test case for empty campsite_type’s name 2023-08-08 19:52:27 +02:00
jordi fita mas 6f6d251cf7 Return HTTP 404 for non-existing admin pages 2023-08-08 19:52:00 +02:00
jordi fita mas d117ce5027 Add public page for campsite type, and function to edit them
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.
2023-08-08 02:45:54 +02:00
jordi fita mas 866af09b50 Move the user role down to company_user relation
I was starting to add the public page for campsite types, creating more
granular row-level security policies for select, insert, update, and
delete, because now the guest users needed to SELECT them and they have
no related company to filter the rows with.  Suddenly, i realized that
the role was wrong in the user relation: a user can be an admin to one
company, and employee to another, and guess to yet another company;
the role should be in the company_user relation instead.

That means that to know the role to set to, the user alone is not enough
and have to know the company as well.  Had to change all the
cookie-related function to accept also the company’s host name, as this
is the information that the Go application has.
2023-08-08 02:22:16 +02:00
jordi fita mas 477865477b Sort the list of localized alternate links
Since the locales is a map, and maps in Go do not have order, sometime
the language switcher was shown in a different order.

I sort by language code, which is as arbitrary as sorting by name, but
makes sense to me.
2023-08-07 11:00:14 +02:00
jordi fita mas e59e6a2a42 Move the locales parameter inside Company struct.
I realized that locales should be company-dependent: we could have two
companies that show pages in a different subset of the application
locales.  It is not the case now, because despite being a “multicompany
application”, it is intended for a single customer, but still makes
sense to include it in Company, even if the subset is the same set as
the application’s.
2023-08-07 10:53:42 +02:00