Skip to content

feat(frontend): improve adding image urls - #286

Merged
sabinem merged 10 commits into
mainfrom
feat/improve-adding-image-urls
Sep 1, 2026
Merged

feat(frontend): improve adding image urls#286
sabinem merged 10 commits into
mainfrom
feat/improve-adding-image-urls

Conversation

@sabinem

@sabinem sabinem commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Put a picture on your hackathon — and see it before anyone else does
Who this is for: the person setting up a hackathon, and everyone who lands on its page.

You can now give your hackathon a picture and know exactly how it will look, before you save. There's a new Manage Public Page tab in the sidebar: your details on the left, and on the right the actual public page, redrawing as you type. Not a mock-up of it — the same page a visitor gets.

What it was like before

You pasted a web address into a box and hoped. There was no way to tell whether it was the right kind of address, and the commonest mistake is invisible: the link your cloud drive hands you after pressing Share points at a web page, not at the picture file. Dropped into a hackathon, it simply shows nothing. You'd find out days later, if at all.

And if your link did work, the picture turned up looking different in four places — a small square in lists, a cropped rectangle on one page, and on the public page a wash so faint (about 9% visible) that most people couldn't tell there was a picture there at all. Anything that wasn't the exact shape the page wanted got its edges chopped off, so a square logo lost its top and bottom.

Meanwhile the member About page — the one page that is actually about the hackathon — showed no picture at all, and the settings form itself was rendering with no styling, because it still referred to colours from a design system that had been removed.

What changed

  • One picture, one look. Your image keeps its own proportions everywhere, shrunk to fit if it's too big. Nothing is ever cropped. A wide banner stays wide, a square logo stays square.
  • The form catches a bad link while you're still in it — it names the mistake ("that's a Google Drive share link"), and for GitHub and Dropbox offers the correct address as a button you press. It never rewrites what you typed behind your back.
  • It tells you how to get a good link at all: right-click the picture in your browser and choose Copy image address. That one menu item is the whole difference, and most people have never had reason to notice it.
  • Members see the picture on the About page now.
  • No more invented pictures. Hackathons without an image used to get a coloured tile whose colour changed on every page load. Now they get a plain empty space, and the only picture shown is the one you chose.

Left out on purpose

  • Project pictures are still small round thumbnails. Fixing that means changing how project pages are laid out, which is a bigger call than this change. The form no longer claims otherwise — it now says "the link works, this is the picture" rather than promising how a page will draw it.
  • You still can't upload a file, only paste an address. There's nowhere to store files yet; that's a backend change, not this one.
  • The landing page's big photo is still a faded background. It's a stock photo of the site itself, not anybody's logo, so the rules above don't apply to it.
  • No "open the live page" link from the editor — the preview beside you already is that page, and following a link out would strand you on the public site with no way back into your hackathon.

Nothing on the platform is uploaded. `Hackathon.logo` and `Project.image`
are single string columns with no file storage behind them, so every
picture is a URL somebody typed — and the commonest thing typed is a
*share* link, the page a cloud drive shows after pressing Share. That
serves HTML rather than bytes: dropped into an `<img>` it fails, and the
only sign of it is a broken-image glyph on somebody else's page, long
after the form was submitted and with no hint of which link did it.

The four forms taking a picture — create hackathon, edit hackathon,
propose project and the project editor — now share one `ImageUrlField`.
It loads the address as it is typed, which is the only honest test:
pattern-matching names the hosts people paste, but cannot tell a direct
URL that 404s from one that works, and those are half the failures. An
`<img>` is also the one probe with no CORS to satisfy and no server of
ours in the middle.

`adviseImageUrl` reports only what is certain — Drive, Photos, OneDrive,
SharePoint, Imgur, Flickr and Unsplash page URLs are known to answer with
a web page. GitHub `blob` and Dropbox links get the direct address
offered as a one-press correction, never applied silently: the address
stays the one the person put there until they say otherwise. Nothing is
rewritten where no documented, stable direct form exists.
A hackathon's logo was stored, editable, and drawn almost nowhere. List
rows showed a generated gradient whether or not a logo was set, and the
public hackathon page did not carry the field out of its loader at all.

Rows on the dashboard and the landing page now draw the logo when there
is one, `object-contain` on a plain ground rather than `cover`: a
hackathon logo is usually wide and often carries its own background, and
a square crop would cut the wordmark in half. The gradient stays the
answer for a hackathon with no logo — it is not a placeholder waiting to
be replaced.

Every surface drawing a stored address now goes through `usableImage`, so
a link that turns out to serve a web page leaves the gradient, or the
hero it never filled, rather than the browser's broken-image glyph. The
tall hero drops `min-h-96` along with the image: a 24rem band holding
nothing is worse than the short hero a hackathon without a logo already
gets.
A member opening About — the one page in the member area whose subject is
the hackathon itself — got a heading and a wall of markdown, no picture,
while the overview one click away showed that same logo as a crisp
thumbnail. The layout draws its hero on /overview alone, and deliberately
so: repeating the hackathon's identity above Participants or Teams pushes
someone's actual errand down the screen. About is the exception that rule
was always going to need.

Shown as a bounded banner rather than the hero's treatment. In a hero the
image is a backdrop with text over it and has to be dimmed to keep that
text legible; here nothing sits on top of it, so there is no contrast to
protect and no reason to dim. Capped at the measure the description below
already uses, so the picture and the prose share one left edge.

The layout's load already carried the hackathon, so this needs no new RPC
and no backend change — About was simply not passing `logo` through.
Both public heroes dim their image twice: an opacity on the `<img>` and
then a scrim over it. It is the product of the two that reaches the
reader, and nothing in either number says so. The hackathon page ran 30%
under a 70% scrim — 9% of the picture — and the landing page 40% under
65%, or 14%. At 9% an image is not a backdrop but a faint tint, and reads
as something that failed to load.

Both now sit at 55% under a 55% scrim: about a quarter of the picture,
18% in dark mode, and the same pair on both surfaces, which they were not
before. The text still sits on `canvas` with the scrim between it and the
image, so the contrast the scrim was protecting is unchanged.

Both files now carry the arithmetic in a comment: neither number means
anything on its own, and the two have to move together.
One field, `Hackathon.logo`, was drawn four ways: a 48px contained square
in list rows, a 224x144 cover crop in the member hero, a full-bleed
wallpaper at 9% behind the public page's title, and a 3:1 cover banner on
About. Same URL, four pictures — and three of them cropped, so a square
wordmark lost its top and bottom while a portrait poster lost most of
itself.

There is no ratio that serves what people actually paste: wide event
banners, square wordmarks with transparent backgrounds, 16:9 photographs
and the occasional poster. A fixed box crops half of them with `cover` or
strands the other half in bars with `contain`. So `StoredImage` imposes no
ratio. The picture keeps its own proportions, bounded by the wrapper's
width and one max-height, and the browser scales it down to fit both —
whole, never cropped, and predictable from a sentence: your picture, at
most this tall, at most this wide.

The public hero stops being wallpaper as a result, which removes the half
of 14ce178 that dimmed it; the landing page's ambiance photo is a
static asset rather than anyone's logo and keeps that change. The hero is
also centred now: left-aligned, the text hung off the edge of a picture
whose width varies with what was pasted, so the column's edge moved from
hackathon to hackathon.
The public route's body moves into `PublicHackathonView`, leaving the
route a twenty-line caller of it. On its own that is a refactor; the
reason for it is the organiser's preview in the next commit, which renders
this same component rather than a copy of the markup.

A preview built from its own markup looks right the day it is written and
lies by the end of the month, and it lies worst exactly where it matters
here — the picture, whose shape nobody can predict from the URL they
pasted. Dates and the status label are derived inside the component for
the same reason, rather than passed in.

`preview` marks the organiser's rendering: the view goes `inert`, so
nothing in it can be clicked, and the Join block is left out — it says the
same thing for every hackathon and nothing on the editor's form changes
it.
Editing a hackathon meant typing a logo URL into a field and finding out
what it looked like by visiting the page afterwards. The form now sits
beside the public page itself, rendered from what is currently typed and
updating as it is typed — the same component the public route renders, not
an impression of it.

`value` becomes a `$bindable` prop on ImageUrlField and MarkdownEditor so
the preview reads the fields rather than keeping a second copy that can
disagree with them. Callers passing `value` one-way are unaffected.

The field's own preview was a 64px square box captioned "This is what will
be shown", which was true of no shape but a square. It draws with
`StoredImage` now, so the form, About and the public page agree. The help
text also says how to get a direct link at all — right-click the picture
and choose Copy image address — because that menu item is the whole
difference between a working URL and a share link, and it is not something
most people have had a reason to notice.

The page was still written against the Skeleton palette it was built on —
`text-surface-950-50`, `border-surface-200-800`, `preset-filled-primary-500`
— none of which exist as utilities any more, so the form had been
rendering unstyled. It is on the theme's own `field`, `field-label`,
`btn-solid` and `--hk-*` tokens now.
The editor was reached from a pencil button beside the Settings heading.
That was the right shape while it was a form for correcting a name; it is
the wrong one now that it carries a preview of the page, which is
somewhere an organiser goes back to rather than a correction to the
heading it sat on.

It carries its own gate, which is the per-entry check `manageNav`'s own
note anticipated: `canEditHackathon` additionally requires the owner be
confirmed, and that route's load answers anyone else with a 403. Listing
it unconditionally would break the stronger promise the section keeps —
never offer a link that then refuses.

Two tests recorded the old arrangement and now record the new one: a
waitlisted owner is withheld exactly this one entry and no other, and
`/manage/edit` lights the tab rather than leaving Settings lit beneath it.
A hackathon without a logo got a generated gradient tile, and which
gradient was chosen by the row's index in the list. The loaders pass the
backend's order through unsorted, so a hackathon's colour changed whenever
that order did — a picture nobody chose, and a different one from one
visit to the next.

The tile is now an empty ground of the same size. It keeps rows with and
without a logo on one left edge, and it invents nothing: the only picture
a row shows is the one an organiser set.
"This is what will be shown" was a promise about a page, and the field is
in no position to make one: a hackathon's picture is drawn whole, and a
project's is cropped to a round thumbnail, from the same field. It now
says the link works and shows the picture it loads, which is true wherever
the field appears. The claim about a page belongs to the preview on Manage
Public Page, which is an actual rendering of that page.

Both project forms name the field for what it holds — a project image
rather than an image — since the hackathon forms next door ask for a logo.
@sabinem
sabinem merged commit 4cfc4cd into main Sep 1, 2026
2 checks passed
@sabinem
sabinem deleted the feat/improve-adding-image-urls branch September 1, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant