feat(frontend): improve adding image urls - #286
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Left out on purpose