Bygga
Type the product name from the box. Bygga shows every assembly step in 3D, with the parts, screws and tools for each one, and reads it out loud.
Compare cheap flights from Bratislava, Vienna, Budapest, Košice and Prague in one search — exact dates, a whole month, or anywhere — with airlines, times and the total trip cost.
Translation pending
LifestyleTypeScript · JavaScriptGitHub @Haldo1231OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Flight comparison site for travellers from Bratislava (BTS), Vienna (VIE), Budapest (BUD), Košice (KSC) and Prague (PRG). Built with Next.js (App Router), TypeScript, Tailwind CSS, Supabase and Vercel. Prices come from the free, cached Travelpayouts data, and flight links earn a small affiliate commission through Aviasales.
Live: https://cheap-flights-app.vercel.app
npm install
npm run dev
Then open http://localhost:3000. The keys the site needs live in a file called .env.local (never commit it):
| Name | What it is for |
|---|---|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY | hostels and weekly deals |
TRAVELPAYOUTS_TOKEN | flight prices |
CRON_SECRET | protects the daily deals job |
UNSPLASH_ACCESS_KEY | only for npm run images (country photos) |
PEXELS_API_KEY | optional: only used by npm run images when Unsplash is unavailable |
Before you publish a change, run npm run build and npm run lint. Both should finish without errors.
The Anywhere results show one photo per country. The photos are chosen ahead of time by a script and saved in
data/country-images.json, so visitors never wait for (or use up the limits of) a photo website.
npm run images # fetch every country that has no photo yet
npm run images -- --wait # when the hourly limit is hit, wait and carry on by itself
npm run images -- --only=IT,ES # only these countries (use --refresh to replace existing ones)
What you need to know:
UNSPLASH_ACCESS_KEY=your-key to .env.local. (The secret key is not needed.)--wait it waits for the next hour by itself (leave the window open).
Applying for "production" status in the Unsplash dashboard raises the limit to 5000 an hour.PEXELS_API_KEY=your-key. If Unsplash is unavailable
(wrong key, limit reached), the script carries on with Pexels.data/country-images-review.md: a list with a link to every chosen photo. Skim it once. To replace a bad pick, delete
its entry in data/country-images.json and run npm run images -- --only=CODE.data/country-images.json and data/country-images-review.md, and push. That is what puts the photos on the site.The rules of the photo sites are followed: photos are loaded straight from their servers, every photo shows "Photo by NAME on Unsplash" (or Pexels) with links, and each photo Unsplash picks is reported to its download endpoint. A country without a photo shows a gradient with its flag, so nothing ever looks broken.
The switch "Fly back to a different airport" is OFF by default and only exists for round trips. A trip that comes back to a different airport is always two separate one-way tickets (a one-way fare out from one of your airports plus a one-way fare back to a DIFFERENT one of BTS, VIE, BUD, KSC or PRG, each with its own price, age and booking button). A round-trip fare already pays for its own way back, so its price is never reused for this.
Narrow searches (exact dates, date range, weekdays), Anywhere and a country's page. With the switch ON there is ONE list, never two sections:
Cost (fake price server, about 150 ms per request, cold cache, 5 home airports, 10 countries): the Anywhere overview asks the free price service for about 65 (date range) to 115 (exact dates) price requests; with the switch ON about 100 more (one-way lists, cached for an hour like everything else) are asked in the second request. A repeat visit within the hour needs none.
Whole month.
/back in month mode. It uses the cheapest cached one-way flight out from the same airport on that day (the
note under the button says which); if none is cached the button is greyed out and says why.Month calendar of flights back (/back, bm=month). It shows the outbound flight and, for one month, the cheapest cached one-way
flight back to each of the OTHER home airports on every day (the airport is written under the price; days without a cached flight are
just empty). Tapping a day lists all of that day's flights to every other airport, cheapest first, with airport, time, airline, price and
age. Choosing one shows the combined trip: outbound + return, the total, and one booking button per flight (both Travelpayouts links with
the affiliate marker, from buildBookingLink), plus "Trip details". Everything is in the address: bm=month, bmon (the month), bday
(the day you tapped), pick (the chosen flight), bx=1 (not back to the airport you left from), plus the outbound flight, the search and
your Whole month selection (the breadcrumb goes back to it). One month of flights back costs 2 price requests per airport (the one-way list,
which is shared between months, and that month's calendar). Measured with a fake price server, production build, cold cache: 8 requests
for the 4 other airports (4 one-way lists + 4 month calendars), 10 for all 5; the next month you look at costs 4 (the calendars; the lists are
already cached), and going back to a month you already saw, or repeating the search within an hour, costs 0. In the real click path (Whole month
page already open, so this month's calendars are cached) the button costs 4 new requests, and turning the Whole month switch ON costs 5.
All prices come from the free, cached Travelpayouts Data API (always asked for in euro). What the site does with them:
expires_at has passed, or that was seen (found_at) more than 72 hours ago,
is left out. If an answer says it is not in euro, the whole answer is thrown away (a dollar price is never shown with a euro sign).found_at, only the
"latest prices" source has it). Otherwise they say "price age not in the data". Nothing is guessed.fetchMonthCalendarDetailed), and the day pairs check the return month
(findBestCombinations with months). The price service is not trusted to stay inside the month it was asked for.getMonthDaysByOrigin / cheapestPerDay in app/flightSources.ts: same requests, same
one-hour cache, so opening the city afterwards costs no new request). A city is only "no matching day" when none of your airports has a fare on
a matching day. Cost: a city checked on the overview or a country page = 5 airports × 2 requests (Whole month and exact dates: 10, date range:
5); a country page opened from the overview needs the same again for each further city, up to 10 cities. Measured on a cold cache in the fake
world, a country with 4 cities: 30 new requests in Whole month (one way and round trip), 17 for a date range, 31 for exact dates (the cheapest-looking
city was already cached by the overview); opened directly from a shared link: 45, 28 and 47. For a real country with 10 cities count about 90-100
(month and exact dates) or 50 (date range). One known difference: in a Whole month round trip the country screen pairs the days inside each
airport (a round trip comes back to the airport it left from), while the city page's "Best combinations" may also pair a flight out from one
airport with a flight back to another; the country screen can then say "no matching day" for a city whose page lists such a pair.Checking real prices on the preview (the cloud tool that builds this code cannot reach Travelpayouts): in Vercel, add the environment
variable DEBUG_PRICES_KEY (any long random text) to the Preview environment and redeploy. Then open
/api/debug-prices?key=YOUR_KEY&origin=BTS&destination=WAW&date=2026-11-09 on the preview address. It asks Travelpayouts live and lists,
for each source, the fares it returned (price, return date, found_at, expires_at, whether the site would use it). Without the key the
page does not exist (404), and the Travelpayouts token is never shown.
The same tool can look at a whole month, to answer "is this price really there on every day, or is one price copied to many days?":
/api/debug-prices?key=YOUR_KEY&origins=BTS,VIE,BUD,KSC,PRG&destination=PMO&month=2026-12&oneWay=true. It lists, day by day, what the city
calendar shows and from which list each price comes, and marks the entries of the "latest prices" list that LOOK LIKE COPIES (no booking site,
flight duration 0 and distance 0: the pattern of the filler rows seen in real answers, the same price repeated on neighbouring dates). Start with
pattern and readThis at the top. It only reads; it changes nothing on the site.
On the search form (step 5) and above every results list there is an optional Max total budget and a Spending style. Results then show an estimated total (flight + what you spend there), mark trips "within budget" or "over budget" and put the within-budget ones first. It works for round trips (including fly-back-to-another-airport), in exact dates, date range, whole month and Anywhere. It is not offered for one-way searches: the section is hidden, switching to One way clears it, and a budget in a one-way address is ignored, so it can never silently filter one-way results.
How a total is worked out (the same on every screen): total = flight price + days of stay x cost of one day.
The flight price is the cached Travelpayouts price (both flights of a round trip). Days of stay = the nights between the two
dates (at least 1). The cost of one day = accommodation + food + transport + other, taken from the
destinations table (hostel_avg_price, food_avg_price, transport_avg_price, other_avg_price) and multiplied for the style.
The three styles live in app/budgetConfig.ts. They are our assumptions, written as multipliers on the destination's numbers:
| Style | Accommodation | Food | Transport | Other | Picture |
|---|---|---|---|---|---|
| Ultra cheap | x 0.8 | x 0.6 | x 0.5 | x 0.3 | dorm bed, street food, walking, free sights |
| Middle | x 1 | x 1 | x 1 | x 1 | exactly the current estimate (hostel, cafés, public transport) |
| Rich | x 3 | x 2.5 | x 3 | x 3 | private room or hotel, restaurants, taxis, paid sights |
Example, Rome (hostel 30, food 25, transport 5, other 10 = 70 a day): Ultra cheap 45 a day, Middle 70, Rich 198.
To make a style cheaper or dearer, change its numbers in app/budgetConfig.ts (nothing else needs touching). The quick budget
buttons (150, 300, 500, 1000) are BUDGET_CHIPS in the same file.
Things to know:
destinations table at all) has no cost data:
the page shows the flight price only, says "No cost data" and never counts it as within budget. Only the 12 destinations in
app/destinationsList.ts have cost data today, so many Anywhere cities show "No cost data".budget=300, level=ultra, within=true) so links can be shared. It is not sent to the price
provider, so changing it never causes new price requests.All dark colours are --dark-* variables in one block in app/globals.css (look for "DARK THEME VALUES"). For a lighter dark mode
raise --dark-paper, --dark-surface, --dark-sand and --dark-line together (each a bit lighter than the one before, so the page,
the cards and the borders stay clearly separate). For a darker one, lower them. --dark-deep is the header, footer and top bands.
Keep --dark-slate (secondary text) light enough to read: it should stay above a 4.5:1 contrast against --dark-surface.
The previous, darker values are written in the comment above the block.
Flags are small SVG pictures in public/flags/ (they come from the flag-icons package, MIT licence), so they also
show on Windows, where flag emoji turn into letters. To refresh them after updating the package: npm run flags.
The site has two kinds of articles. They are plain text files in the content/ folder, so there is no database and
nothing to log into:
/trips): your real trips, with what they really cost. Files go in content/trips/./guides): articles such as "Where to fly from: Košice vs Vienna vs Budapest". Files go in content/guides/.The only trip that exists today is content/trips/sample-trip.md. It is a fake sample (marked draft: true and
"SAMPLE, NOT REAL"). It shows up when you run the site on your computer and is hidden on the live site.
Delete it when you have your first real trip.
content/_templates/trip.md to content/trips/<your-slug>.md. The file name must be the slug plus .md
(for example rome-with-a-friend.md) and the slug: line inside must be the same word.--- lines). The numbers are euro, per person.
Below the second --- write your story in Markdown (## Heading, **bold**, - list).alt: (one sentence describing it, for blind visitors and Google).npm run dev, then open http://localhost:3000/trips. Drafts show up there with a
"Draft, development only" label.draft: true to draft: false (or delete the line), run npm run build, commit and push.
The live site only changes after you push, because the files are part of the website.If a line is missing or misspelled, npm run build stops and tells you which file and which field, for example
content/trips/rome.md: field "nights" is missing. Nothing broken can go live.
The lines of a trip file:
| Line | Needed? | What it is |
|---|---|---|
title, slug, date, country | yes | name of the page, the web address word, YYYY-MM-DD, the country |
nights | yes | number of nights (1 or more) |
days | no | number of days; if left out it is nights + 1 |
travellers | yes | for example solo or with a friend |
origin | yes | 3-letter airport code you flew from, for example KSC |
airline | yes | the airline you flew with |
flightPricePerPerson | yes | euro, both ways, per person |
accommodationPerNight, foodPerDay, transportPerDay | yes | euro per night / per day |
otherPerDay | no | euro per day for tickets, tours, souvenirs |
destination | no | one of the 12 destination words (rome, milan, ...). Turns on the "compare with our estimate" box and the flight search buttons |
description | no | one sentence for Google and link previews (max 200 characters) |
tips, mistakes | no | lists: "tips" and "what I would do differently" |
cover | no | src (file name) and alt (description) |
photos | no | list of src, alt (required), caption (optional) |
draft | no | true hides it on the live site. Default is false |
language | no | en, sk, de ... (default ) |
How the total is counted, so your numbers are easy to check:
The "Compare with our estimate" box on a trip page only shows a row when both your number and our estimate exist. Nothing is guessed or filled in.
Copy content/_templates/guide.md to content/guides/<your-slug>.md, fill in the lines (title, slug,
description, date are needed; tags, relatedDestinations, cover, draft, language are optional) and write the
article below the second ---. Publishing works the same way as for trips. The relatedDestinations list adds a block
under the article with each destination's cheapest cached fare and a link to search flights.
Your own photos go in public/trips/<slug>/ (for guides public/guides/<slug>/). Phone photos are big and carry
hidden information, including the GPS place where you took them. The script below makes them website-sized and
removes that hidden information. Steps:
photos-inbox/<slug>/ and copy your original photos into it. This folder is ignored by git,
so the originals are never uploaded.npm run optimize-photos -- <slug> (for a guide add --kind=guides).public/trips/<slug>/, and the script prints the lines to
paste into your file under photos:. Replace each TODO alt text with a real description.npm run optimize-photos -- <slug> --check says if any photo still contains
location data.If a photo listed in a file is missing in the folder, or has no alt, npm run build fails and names it.
Checklist before you publish a trip:
alt).npm run optimize-photos (no GPS data) and npm run build passes./about) has marked TODO boxes where you write about yourself. While a TODO remains, the page is
hidden from search engines and left out of the sitemap. Once it is written, delete the boxes and set ABOUT_HAS_TODO to false in app/lib/aboutStatus.ts: the page then appears in search engines and the sitemap./disclosure explains that some flight links are affiliate links. A short note is also in the footer.public/ can still be opened by someone who
knows the exact web address. Do not put photos there that you would not show publicly./sitemap.xml and /robots.txt are generated; they list only published (not draft) trips and guides.On a one-way flight (a one-way search result, the trip page of a one-way flight, or a one-way flight picked in Whole month) the button
"Find a flight back" opens /back. It shows the outbound flight and asks for the return days (a date range, by default the two weeks
after the outbound flight, or one exact date). It then lists the cached one-way flights from that destination to any of BTS, VIE, BUD, KSC
and PRG, cheapest first, with airport, date, time, airline, price and how old the price is. Picking one shows the combined trip: outbound +
return, the total price, and one booking button for each flight (both are Travelpayouts links with the affiliate marker, built by
buildBookingLink), plus "Trip details". If nothing is cached for those days the page says so ("No cached flights back from ... Try a
wider date range or another day"). The flight, the days and the pick are all in the address, so the page can be shared and Back works.
The data comes from /api/return-flights (getReturnFares: the one-way list plus calendar entries without a return date; limit says how
many of the cheapest come back, 60 by default and up to 400 for a whole month). The page can also show a month calendar instead of days
(bm=month, see "Flying back to a different home airport").
See AGENTS.md for the map of the code, and PROGRESS.md for what was changed and why.
en