rewrite in svelte 😭

This commit is contained in:
hazel 2023-01-12 10:37:22 -06:00
commit 96d6068848
No known key found for this signature in database
GPG key ID: 215AF1F81F86940E

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -1,7 +1,38 @@
# BLÅHAJ Radar
# create-svelte
Website that displays listings for BLÅHAJ in various countries.
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
[View website](https://hazy.su/blahaj)
## Creating a project
Inspired by [shark-radar](https://git.lavender.software/charlotte/shark-radar), a Python script displaying BLÅHAJ listings in the UK.
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

808
app.js
View file

@ -1,808 +0,0 @@
let listingsDiv = document.getElementById('listings')
let ikeaData = {
gb: {
name: "United Kingdom",
abbrv: "the UK",
url: "https://www.ikea.com/gb/en/p/blahaj-soft-toy-shark-30373588/",
apiUrl: "cia/availabilities/ru/gb?itemNos=30373588&expand=StoresList,Restocks,SalesLocations",
stores: [{
value: "113",
name: "Belfast"
}, {
value: "264",
name: "Bristol"
}, {
value: "267",
name: "Cardiff"
}, {
value: "144",
name: "Croydon"
}, {
value: "548",
name: "Exeter"
}, {
value: "143",
name: "Gateshead"
}, {
value: "567",
name: "Greenwich"
}, {
value: "261",
name: "Leeds"
}, {
value: "186",
name: "Manchester"
}, {
value: "185",
name: "Milton Keynes"
}, {
value: "263",
name: "Nottingham"
}, {
value: "461",
name: "Reading"
}, {
value: "519",
name: "Sheffield"
}, {
value: "125",
name: "Southampton"
}, {
value: "255",
name: "Tottenham"
}, {
value: "140",
name: "Warrington"
}, {
value: "266",
name: "Glasgow"
}, {
value: "265",
name: "Edinburgh"
}, {
value: "262",
name: "Lakeside"
}, {
value: "142",
name: "Birmingham"
}, {
value: "141",
name: "Wembley"
}]
},
us: {
name: "United States",
abbrv: "the US",
url: "https://www.ikea.com/us/en/p/blahaj-soft-toy-shark-90373590/",
apiUrl: "cia/availabilities/ru/us?itemNos=90373590&expand=StoresList,Restocks,SalesLocations",
stores: [{
value: "257",
name: "Atlanta, GA",
storePageUrl: "https://www.ikea.com/us/en/stores/atlanta/"
}, {
value: "152",
name: "Baltimore, MD",
storePageUrl: "https://www.ikea.com/us/en/stores/baltimore/"
}, {
value: "170",
name: "Bolingbrook, IL",
storePageUrl: "https://www.ikea.com/us/en/stores/bolingbrook/"
}, {
value: "921",
name: "Brooklyn, NY",
storePageUrl: "https://www.ikea.com/us/en/stores/brooklyn/"
}, {
value: "399",
name: "Burbank, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/burbank/"
}, {
value: "026",
name: "Canton, MI",
storePageUrl: "https://www.ikea.com/us/en/stores/canton/"
}, {
value: "162",
name: "Carson, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/carson/"
}, {
value: "064",
name: "Centennial, CO",
storePageUrl: "https://www.ikea.com/us/en/stores/centennial/"
}, {
value: "067",
name: "Charlotte, NC",
storePageUrl: "https://www.ikea.com/us/en/stores/charlotte/"
}, {
value: "411",
name: "College Park, MD",
storePageUrl: "https://www.ikea.com/us/en/stores/college-park/"
}, {
value: "511",
name: "Columbus, OH",
storePageUrl: "https://www.ikea.com/us/en/stores/columbus/"
}, {
value: "211",
name: "Conshohocken, PA",
storePageUrl: "https://www.ikea.com/us/en/stores/conshohocken/"
}, {
value: "167",
name: "Costa Mesa, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/costa-mesa/"
}, {
value: "413",
name: "Covina, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/covina/"
}, {
value: "103",
name: "Draper, UT",
storePageUrl: "https://www.ikea.com/us/en/stores/draper/"
}, {
value: "347",
name: "East Palo Alto, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/east-palo-alto/"
}, {
value: "154",
name: "Elizabeth, NJ",
storePageUrl: "https://www.ikea.com/us/en/stores/elizabeth/"
}, {
value: "165",
name: "Emeryville, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/emeryville/"
}, {
value: "536",
name: "Fishers, IN",
storePageUrl: "https://www.ikea.com/us/en/stores/fishers/"
}, {
value: "183",
name: "Frisco, TX",
storePageUrl: "https://www.ikea.com/us/en/stores/frisco/"
}, {
value: "535",
name: "Grand Prairie, TX",
storePageUrl: "https://www.ikea.com/us/en/stores/grand-prairie/"
}, {
value: "379",
name: "Houston, TX",
storePageUrl: "https://www.ikea.com/us/en/stores/houston/"
}, {
value: "537",
name: "Jacksonville, FL",
storePageUrl: "https://www.ikea.com/us/en/stores/jacksonville/"
}, {
value: "462",
name: "Las Vegas, NV",
storePageUrl: "https://www.ikea.com/us/en/stores/las-vegas/"
}, {
value: "570",
name: "Live Oak, TX",
storePageUrl: "https://www.ikea.com/us/en/stores/live-oak/"
}, {
value: "156",
name: "Long Island, NY",
storePageUrl: "https://www.ikea.com/us/en/stores/long-island/"
}, {
value: "508",
name: "Memphis, TN",
storePageUrl: "https://www.ikea.com/us/en/stores/memphis/"
}, {
value: "374",
name: "Merriam, KS",
storePageUrl: "https://www.ikea.com/us/en/stores/merriam/"
}, {
value: "327",
name: "Miami, FL",
storePageUrl: "https://www.ikea.com/us/en/stores/miami/"
}, {
value: "213",
name: "New Haven, CT",
storePageUrl: "https://www.ikea.com/us/en/stores/new-haven/"
}, {
value: "569",
name: "Norfolk, VA",
storePageUrl: "https://www.ikea.com/us/en/stores/norfolk/"
}, {
value: "560",
name: "Oak Creek, WI",
storePageUrl: "https://www.ikea.com/us/en/stores/oak-creek/"
}, {
value: "145",
name: "Orlando, FL",
storePageUrl: "https://www.ikea.com/us/en/stores/orlando/"
}, {
value: "409",
name: "Paramus, NJ",
storePageUrl: "https://www.ikea.com/us/en/stores/paramus/"
}, {
value: "153",
name: "Pittsburgh, PA",
storePageUrl: "https://www.ikea.com/us/en/stores/pittsburgh/"
}, {
value: "028",
name: "Portland, OR",
storePageUrl: "https://www.ikea.com/us/en/stores/portland/"
}, {
value: "622",
name: "Queens, NY",
storePageUrl: "https://www.ikea.com/us/en/stores/queens/"
}, {
value: "488",
name: "Renton, WA",
storePageUrl: "https://www.ikea.com/us/en/stores/renton/"
}, {
value: "027",
name: "Round Rock, TX",
storePageUrl: "https://www.ikea.com/us/en/stores/round-rock/"
}, {
value: "166",
name: "San Diego, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/san-diego/"
}, {
value: "210",
name: "Schaumburg, IL",
storePageUrl: "https://www.ikea.com/us/en/stores/schaumburg/"
}, {
value: "215",
name: "South Philadelphia, PA",
storePageUrl: "https://www.ikea.com/us/en/stores/south-philadelphia/"
}, {
value: "410",
name: "St. Louis, MO",
storePageUrl: "https://www.ikea.com/us/en/stores/st-louis/"
}, {
value: "158",
name: "Stoughton, MA",
storePageUrl: "https://www.ikea.com/us/en/stores/stoughton/"
}, {
value: "207",
name: "Sunrise, FL",
storePageUrl: "https://www.ikea.com/us/en/stores/sunrise/"
}, {
value: "042",
name: "Tampa, FL",
storePageUrl: "https://www.ikea.com/us/en/stores/tampa/"
}, {
value: "209",
name: "Tempe, AZ",
storePageUrl: "https://www.ikea.com/us/en/stores/tempe/"
}, {
value: "212",
name: "Twin Cities, MN",
storePageUrl: "https://www.ikea.com/us/en/stores/twin-cities/"
}, {
value: "175",
name: "West Chester, OH",
storePageUrl: "https://www.ikea.com/us/en/stores/west-chester/"
}, {
value: "157",
name: "West Sacramento, CA",
storePageUrl: "https://www.ikea.com/us/en/stores/west-sacramento/"
}, {
value: "168",
name: "Woodbridge, VA",
storePageUrl: "https://www.ikea.com/us/en/stores/woodbridge/"
}]
},
ca: {
name: "Canada",
url: "https://www.ikea.com/ca/en/p/blahaj-soft-toy-shark-90373590/",
apiUrl: "cia/availabilities/ru/ca?itemNos=90373590&expand=StoresList,Restocks,SalesLocations",
stores: [{
value: "414",
name: "Boucherville",
storePageUrl: "https://www.ikea.com/ca/en/stores/boucherville/"
}, {
value: "040",
name: "Burlington",
storePageUrl: "https://www.ikea.com/ca/en/stores/burlington/"
}, {
value: "216",
name: "Calgary",
storePageUrl: "https://www.ikea.com/ca/en/stores/calgary/"
}, {
value: "313",
name: "Coquitlam",
storePageUrl: "https://www.ikea.com/ca/en/stores/coquitlam/"
}, {
value: "349",
name: "Edmonton",
storePageUrl: "https://www.ikea.com/ca/en/stores/edmonton/"
}, {
value: "256",
name: "Etobicoke",
storePageUrl: "https://www.ikea.com/ca/en/stores/etobicoke/"
}, {
value: "529",
name: "Halifax",
storePageUrl: "https://www.ikea.com/ca/en/stores/halifax/"
}, {
value: "039",
name: "Montreal",
storePageUrl: "https://www.ikea.com/ca/en/stores/montreal/"
}, {
value: "149",
name: "North York",
storePageUrl: "https://www.ikea.com/ca/en/stores/north-york/"
}, {
value: "004",
name: "Ottawa",
storePageUrl: "https://www.ikea.com/ca/en/stores/ottawa/"
}, {
value: "559",
name: "Quebec City",
storePageUrl: "https://www.ikea.com/ca/en/stores/quebec/"
}, {
value: "003",
name: "Richmond",
storePageUrl: "https://www.ikea.com/ca/en/stores/richmond/"
}, {
value: "372",
name: "Vaughan",
storePageUrl: "https://www.ikea.com/ca/en/stores/vaughan/"
}, {
value: "249",
name: "Winnipeg",
storePageUrl: "https://www.ikea.com/ca/en/stores/winnipeg/"
}]
},
sw: {
name: "Sweden",
url: "https://www.ikea.com/se/sv/p/blahaj-mjukleksak-haj-30373588/",
apiUrl: "cia/availabilities/ru/se?itemNos=30373588&expand=StoresList,Restocks,SalesLocations",
stores: [{
value: "248",
name: "Borlänge",
storePageUrl: "https://www.ikea.com/se/sv/stores/borlange/"
}, {
value: "122",
name: "Gävle",
storePageUrl: "https://www.ikea.com/se/sv/stores/gavle/"
}, {
value: "398",
name: "Göteborg Bäckebol",
storePageUrl: "https://www.ikea.com/se/sv/stores/goteborg-backebol/"
}, {
value: "014",
name: "Göteborg Kållered",
storePageUrl: "https://www.ikea.com/se/sv/stores/goteborg-kallered/"
}, {
value: "470",
name: "Haparanda",
storePageUrl: "https://www.ikea.com/se/sv/stores/haparandatornio/"
}, {
value: "468",
name: "Helsingborg",
storePageUrl: "https://www.ikea.com/se/sv/stores/helsingborg/"
}, {
value: "109",
name: "Jönköping",
storePageUrl: "https://www.ikea.com/se/sv/stores/jonkoping/"
}, {
value: "469",
name: "Kalmar",
storePageUrl: "https://www.ikea.com/se/sv/stores/kalmar/"
}, {
value: "471",
name: "Karlstad",
storePageUrl: "https://www.ikea.com/se/sv/stores/karlstad/"
}, {
value: "017",
name: "Linköping",
storePageUrl: "https://www.ikea.com/se/sv/stores/linkoping/"
}, {
value: "445",
name: "Malmö",
storePageUrl: "https://www.ikea.com/se/sv/stores/malmo/"
}, {
value: "019",
name: "Stockholm Barkarby",
storePageUrl: "https://www.ikea.com/se/sv/stores/stockholm-barkarby/"
}, {
value: "012",
name: "Stockholm Kungens Kurva",
storePageUrl: "https://www.ikea.com/se/sv/stores/stockholm-kungens-kurva/"
}, {
value: "467",
name: "Sundsvall",
storePageUrl: "https://www.ikea.com/se/sv/stores/sundsvall/"
}, {
value: "053",
name: "Uddevalla",
storePageUrl: "https://www.ikea.com/se/sv/stores/uddevalla/"
}, {
value: "416",
name: "Umeå",
storePageUrl: "https://www.ikea.com/se/sv/stores/umea/"
}, {
value: "070",
name: "Uppsala",
storePageUrl: "https://www.ikea.com/se/sv/stores/uppsala/"
}, {
value: "020",
name: "Västerås",
storePageUrl: "https://www.ikea.com/se/sv/stores/vasteras/"
}, {
value: "268",
name: "Älmhult",
storePageUrl: "https://www.ikea.com/se/sv/stores/almhult/"
}, {
value: "106",
name: "Örebro",
storePageUrl: "https://www.ikea.com/se/sv/stores/orebro/"
}]
},
de: {
name: "Deutschland",
url: "https://www.ikea.com/de/de/p/blahaj-stoffspielzeug-hai-30373588/",
apiUrl: "cia/availabilities/ru/de?itemNos=30373588&expand=StoresList,Restocks,SalesLocations",
stores: [{
value: "066",
name: "Augsburg",
storePageUrl: "https://www.ikea.com/de/de/stores/augsburg/"
}, {
value: "324",
name: "Berlin-Lichtenberg",
storePageUrl: "https://www.ikea.com/de/de/stores/berlin-lichtenberg/"
}, {
value: "394",
name: "Berlin-Spandau",
storePageUrl: "https://www.ikea.com/de/de/stores/berlin-spandau"
}, {
value: "421",
name: "Berlin-Tempelhof",
storePageUrl: "https://www.ikea.com/de/de/stores/berlin-tempelhof/"
}, {
value: "129",
name: "Berlin-Waltersdorf",
storePageUrl: "https://www.ikea.com/de/de/stores/berlin-waltersdorf/"
}, {
value: "119",
name: "Bielefeld",
storePageUrl: "https://www.ikea.com/de/de/stores/bielefeld/"
}, {
value: "117",
name: "Braunschweig",
storePageUrl: "https://www.ikea.com/de/de/stores/braunschweig/"
}, {
value: "412",
name: "Bremerhaven",
storePageUrl: "https://www.ikea.com/de/de/stores/bremerhaven/"
}, {
value: "228",
name: "Brinkum",
storePageUrl: "https://www.ikea.com/de/de/stores/brinkum/"
}, {
value: "118",
name: "Chemnitz",
storePageUrl: "https://www.ikea.com/de/de/stores/chemnitz/"
}, {
value: "223",
name: "Dortmund",
storePageUrl: "https://www.ikea.com/de/de/stores/dortmund/"
}, {
value: "221",
name: "Dresden",
storePageUrl: "https://www.ikea.com/de/de/stores/dresden/"
}, {
value: "425",
name: "Duisburg",
storePageUrl: "https://www.ikea.com/de/de/stores/duisburg/"
}, {
value: "321",
name: "Düsseldorf",
storePageUrl: "https://www.ikea.com/de/de/stores/duesseldorf/"
}, {
value: "396",
name: "Erfurt",
storePageUrl: "https://www.ikea.com/de/de/stores/erfurt/"
}, {
value: "148",
name: "Essen",
storePageUrl: "https://www.ikea.com/de/de/stores/essen/"
}, {
value: "393",
name: "Frankfurt",
storePageUrl: "https://www.ikea.com/de/de/stores/frankfurt/"
}, {
value: "320",
name: "Freiburg",
storePageUrl: "https://www.ikea.com/de/de/stores/freiburg/"
}, {
value: "226",
name: "Großburgwedel",
storePageUrl: "https://www.ikea.com/de/de/stores/grossburgwedel/"
}, {
value: "139",
name: "Halle/Leipzig",
storePageUrl: "https://www.ikea.com/de/de/stores/halle-leipzig/"
}, {
value: "245",
name: "Hamburg-Altona",
storePageUrl: "https://www.ikea.com/de/de/stores/hamburg-altona/"
}, {
value: "325",
name: "Hamburg-Moorfleet",
storePageUrl: "https://www.ikea.com/de/de/stores/hamburg-moorfleet/"
}, {
value: "146",
name: "Hamburg-Schnelsen",
storePageUrl: "https://www.ikea.com/de/de/stores/hamburg-schnelsen/"
}, {
value: "222",
name: "Hanau",
storePageUrl: "https://www.ikea.com/de/de/stores/hanau/"
}, {
value: "187",
name: "Hannover EXPO-Park",
storePageUrl: "https://www.ikea.com/de/de/stores/hannover-expo-park/"
}, {
value: "494",
name: "Kaarst",
storePageUrl: "https://www.ikea.com/de/de/stores/kaarst/"
}, {
value: "430",
name: "Kaiserslautern",
storePageUrl: "https://www.ikea.com/de/de/stores/kaiserslautern/"
}, {
value: "323",
name: "Kamen",
storePageUrl: "https://www.ikea.com/de/de/stores/kamen/"
}, {
value: "551",
name: "Karlsruhe",
storePageUrl: "https://www.ikea.com/de/de/stores/karlsruhe/"
}, {
value: "174",
name: "Kassel",
storePageUrl: "https://www.ikea.com/de/de/stores/kassel/"
}, {
value: "333",
name: "Kiel",
storePageUrl: "https://www.ikea.com/de/de/stores/kiel/"
}, {
value: "332",
name: "Koblenz",
storePageUrl: "https://www.ikea.com/de/de/stores/koblenz/"
}, {
value: "102",
name: "Köln-Am Butzweilerhof",
storePageUrl: "https://www.ikea.com/de/de/stores/koeln-am-butzweilerhof/"
}, {
value: "147",
name: "Köln-Godorf",
storePageUrl: "https://www.ikea.com/de/de/stores/koeln-godorf/"
}, {
value: "289",
name: "Lübeck",
storePageUrl: "https://www.ikea.com/de/de/stores/luebeck/"
}, {
value: "225",
name: "Ludwigsburg",
storePageUrl: "https://www.ikea.com/de/de/stores/ludwigsburg/"
}, {
value: "520",
name: "Magdeburg",
storePageUrl: "https://www.ikea.com/de/de/stores/magdeburg/"
}, {
value: "397",
name: "Mannheim",
storePageUrl: "https://www.ikea.com/de/de/stores/mannheim/"
}, {
value: "343",
name: "München-Brunnthal",
storePageUrl: "https://www.ikea.com/de/de/stores/muenchen-brunnthal/"
}, {
value: "063",
name: "München-Eching",
storePageUrl: "https://www.ikea.com/de/de/stores/muenchen-eching/"
}, {
value: "326",
name: "Nürnberg-Fürth",
storePageUrl: "https://www.ikea.com/de/de/stores/nuernberg-fuerth/"
}, {
value: "069",
name: "Oldenburg",
storePageUrl: "https://www.ikea.com/de/de/stores/oldenburg/"
}, {
value: "184",
name: "Osnabrück",
storePageUrl: "https://www.ikea.com/de/de/stores/osnabrueck/"
}, {
value: "617",
name: "Potsdam",
storePageUrl: "https://www.ikea.com/de/de/stores/potsdam/"
}, {
value: "229",
name: "Regensburg",
storePageUrl: "https://www.ikea.com/de/de/stores/regensburg/"
}, {
value: "092",
name: "Rostock",
storePageUrl: "https://www.ikea.com/de/de/stores/rostock/"
}, {
value: "227",
name: "Saarlouis",
storePageUrl: "https://www.ikea.com/de/de/stores/saarlouis/"
}, {
value: "369",
name: "Siegen",
storePageUrl: "https://www.ikea.com/de/de/stores/siegen/"
}, {
value: "224",
name: "Sindelfingen",
storePageUrl: "https://www.ikea.com/de/de/stores/sindelfingen/"
}, {
value: "328",
name: "Ulm",
storePageUrl: "https://www.ikea.com/de/de/stores/ulm/"
}, {
value: "322",
name: "Wallau",
storePageUrl: "https://www.ikea.com/de/de/stores/wallau/"
}, {
value: "075",
name: "Walldorf",
storePageUrl: "https://www.ikea.com/de/de/stores/walldorf/"
}, {
value: "493",
name: "Wetzlar",
storePageUrl: "https://www.ikea.com/de/de/stores/wetzlar/"
}, {
value: "492",
name: "Wuppertal",
storePageUrl: "https://www.ikea.com/de/de/stores/wuppertal/"
}, {
value: "124",
name: "Würzburg",
storePageUrl: "https://www.ikea.com/de/de/stores/wuerzburg/"
}]
}
}
function asciiFix(t) {
return utf8.decode(t)
}
async function get(path) {
let req
try {
req = await fetch(`https://api.ingka.ikea.com/${path}`, {
headers: {
'Accept': 'application/json;version=2',
'X-Client-ID': 'b6c117e5-ae61-4ef5-b4cc-e0b1e37f0631'
}
})
return await req.json()
} catch (error) {
document.getElementById('error').style.display = 'block'
document.getElementById('error-message').innerText = error.message
if (document.getElementById('listings')) document.getElementById('listings').remove()
throw error
}
}
function parseStock(availability, code) {
let store = ikeaData[code].stores.find(e => {
return e.value === availability.classUnitKey.classUnitCode
})
if (!store) {
return {
err: true,
store: 'unknown',
quantity: 0
}
}
let nextRestock = undefined
if (availability.buyingOption?.cashCarry?.availability?.restocks) {
nextRestock = availability.buyingOption.cashCarry.availability.restocks[0]
}
let quantity = availability.buyingOption?.cashCarry?.availability?.quantity??null
if (quantity===null) {
return {
store: store.name,
message: 'unavailable',
quantity: 0,
nextRestock: nextRestock
}
}
return {
store: store.name,
quantity: quantity,
nextRestock: nextRestock
}
}
function formatDate(str) {
let date = new Date(str)
return new Intl.DateTimeFormat('en-US').format(date)
}
function makeStockElem(parsed) {
let elem = document.createElement('div')
elem.classList.add('listing')
if (parsed.quantity&&parsed.quantity > 0) elem.classList.add('positive')
else elem.classList.add('negative')
let title = document.createElement('h3')
title.innerText = parsed.store
let quantity = document.createElement('p')
quantity.classList.add('quantity')
quantity.innerText = (parsed.message)?parsed.message:`${parsed.quantity} sharks`
elem.appendChild(title)
elem.appendChild(quantity)
if (parsed.nextRestock) {
let nextRestock = document.createElement('p')
nextRestock.classList.add('restock-date')
let dateEarliest = formatDate(parsed.nextRestock.earliestDate)
let dateLatest = formatDate(parsed.nextRestock.latestDate)
let range = (dateEarliest === dateLatest) ? dateEarliest : `${dateEarliest}-${dateLatest}`
nextRestock.innerText = `Restocking ${parsed.nextRestock.quantity} sharks on ${range}`
elem.appendChild(nextRestock)
}
return elem
}
async function loadCountryListings(countrycode) {
let countriesListingsDiv = document.createElement('details')
let heading = document.createElement('summary')
heading.innerText = ikeaData[countrycode].name + ' (Loading)'
let link = document.createElement('a')
link.setAttribute('href', ikeaData[countrycode].url)
link.innerText = `See the listings for BLÅHAJ in ${ikeaData[countrycode].abbrv??ikeaData[countrycode].name}`
countriesListingsDiv.appendChild(heading)
countriesListingsDiv.appendChild(link)
listingsDiv.appendChild(countriesListingsDiv)
let listingsContainer = document.createElement('div')
let dat
try {
dat = await get(ikeaData[countrycode].apiUrl)
} catch (error) {
throw error
}
let listings = dat.availabilities.map(e => parseStock(e, countrycode))
listings.sort((a, b) => {
return b.quantity - a.quantity;
})
for (let i in listings) {
if (!listings[i].err) listingsContainer.appendChild(makeStockElem(listings[i]))
}
countriesListingsDiv.appendChild(listingsContainer)
heading.innerText = ikeaData[countrycode].name
}
async function start() {
let debug = false
let countryCodes = Object.keys(ikeaData).sort((a, b) => ikeaData[a].name.localeCompare(ikeaData[b].name))
for (let i in countryCodes) {
if (debug) {
// Fix ASCII encoding stuff cause idontknow. i def implemented this poorly too but im lazy and dont know the proper way to do this
ikeaData[countryCodes[i]].stores = ikeaData[countryCodes[i]].stores.map(e => {
let name
try {
name = asciiFix(e.name)
} catch (error) {
name = e.name
}
return {
value: e.value,
name,
storePageUrl: e.storePageUrl
}
})
}
try {
loadCountryListings(countryCodes[i])
} catch (error) {
return
}
}
// log so i can replace it if i add more things
if (debug) console.log(JSON.stringify(ikeaData, null, 2))
}
start()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

0
docs/.nojekyll Normal file
View file

View file

@ -0,0 +1 @@
*,*:before,*:after{box-sizing:border-box}:root{--bg: #180e2c;--fg: white;--accent-clr: #a87dff;--link: #a87dff;--font-family: "Shippori Antique B1", sans-serif}body{overflow-x:hidden;margin:0;background-color:var(--bg);color:var(--fg);font-family:var(--font-family)}h1,h2,h3,h4,h5,h6,p{margin:0}a{position:relative;text-decoration:none;color:var(--link)}a:after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background-color:currentColor;opacity:0;transform-origin:left center;transform:scaleX(0);transition:transform .3s,opacity .3s;transition-delay:50ms}a:hover:after{opacity:1;transform:scaleX(1)}img{max-width:100%}ol,ul{padding:0;margin:0}button{display:block;background:none;color:inherit;border:none;margin:0;padding:0;font:inherit;cursor:pointer}.layout{display:flex;flex-direction:column;align-items:center;min-height:100vh;min-height:100svh}.hero{width:100%;min-height:16rem;position:relative;display:flex;padding:2rem;gap:1rem;justify-content:center;align-items:center;background-color:#0003}.hero:after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;z-index:1;background-image:linear-gradient(90deg,#130b23a8 300px,#130b2338 600px)}.hero-message{z-index:2}.hero-cover{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:absolute;top:0;right:0;height:100%;width:100%;max-width:800px;-o-object-fit:cover;object-fit:cover;-o-object-position:left 60%;object-position:left 60%;-webkit-mask-image:linear-gradient(100deg,rgba(0,0,0,0) 10%,rgba(0,0,0,.01) 15%,rgba(0,0,0,.05) 20%,rgba(0,0,0,.5) 60%,white 100%);mask-image:linear-gradient(100deg,rgba(0,0,0,0) 10%,rgba(0,0,0,.01) 15%,rgba(0,0,0,.05) 20%,rgba(0,0,0,.5) 60%,white 100%)}.blahaj-select-buttons{display:flex;width:100%;gap:1rem}.blahaj-select-buttons button{position:relative;flex-grow:1;padding:.4rem;background-color:#0003;border-radius:6px}.blahaj-select-buttons button:after{content:"";background-color:var(--accent-clr);position:absolute;left:0;right:0;bottom:0;height:2px;opacity:0;transition:opacity .2s}.blahaj-select-buttons button[selected]:after{opacity:1}.display-image-wrapper{display:grid;width:100%;height:10rem;background-color:#0003;border-radius:6px;position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none}.display-image{grid-column:1/2;grid-row:1/2;position:relative;width:-moz-fit-content;width:fit-content;height:100%;margin:0 auto}.display-image img{width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.display-image-badge{position:absolute;right:.2rem;top:1.2rem;transform:rotate(8deg);padding:.2rem .4rem;background-color:var(--accent-clr);border-radius:5px;box-shadow:0 1px 10px 12px #00000080}.main-content{flex-grow:1;display:flex;flex-direction:column;padding:1rem;gap:1rem;width:100%;max-width:800px;margin:0 auto}.country-filter-search{background-color:#0003;border:none;padding:.8rem .4rem;color:inherit;font:inherit;border-radius:6px}.listings-wrapper{display:grid;overflow:hidden}.listings{grid-column:1/2;grid-row:1/2;display:flex;flex-direction:column;gap:1rem}.country-listings{background-color:#0003;border-radius:6px;padding:.5rem;overflow:hidden}.country-listings-emoji{position:relative;padding-right:1.25rem}.country-listings-emoji:after{z-index:15;background-image:url(../../../blobhaj.svg);content:"";display:block;position:absolute;right:0px;width:1.5rem;top:0px;height:2rem;background-size:100%;background-repeat:no-repeat;background-position:center bottom}.country-listings-open-button{width:100%;display:flex;align-items:center}.country-listings-open-button h2{text-align:left;width:-moz-fit-content;width:fit-content;margin:0}.country-listings-open-button h2:after{display:inline-block;margin-left:.5rem;content:"\21b4";transition:transform .2s;transition-delay:50ms;transition-timing-function:cubic-bezier(.61,.76,.34,1.73)}.country-listings-open-button:hover h2:after{transform:translate(4px)}.country-listings-content{margin-top:1rem}.listing{display:block;background:rgb(0 0 0 / .2);border-radius:.5rem;padding:.25rem;margin:1rem 0}.listing.positive h3{color:#4eff93}.listing.negative h3{color:#c83867}.address{font-size:.8rem;opacity:.5}.links{background-color:#0003;display:flex;flex-direction:column;gap:1rem;width:100%;margin-top:1rem;padding:1rem;text-align:center}

View file

@ -0,0 +1 @@
import{_ as r}from"./_layout-da46b06b.js";import{default as t}from"../components/pages/_layout.svelte-76471106.js";export{t as component,r as universal};

View file

@ -0,0 +1 @@
import{default as t}from"../components/error.svelte-0ab2467a.js";export{t as component};

View file

@ -0,0 +1 @@
import{default as t}from"../components/pages/_page.svelte-a3469a4f.js";export{t as component};

View file

@ -0,0 +1 @@
const e=!0,r=Object.freeze(Object.defineProperty({__proto__:null,prerender:!0},Symbol.toStringTag,{value:"Module"}));export{r as _,e as p};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
let a="",e="";function t(s){a=s.base,e=s.assets||a}export{e as a,a as b,t as s};

View file

@ -0,0 +1 @@
import{B as d,s as w}from"./index-bf58d8cc.js";import{a as E}from"./paths-b4419565.js";const c=[];function p(e,t=d){let n;const o=new Set;function s(a){if(w(e,a)&&(e=a,n)){const u=!c.length;for(const l of o)l[1](),c.push(l,e);if(u){for(let l=0;l<c.length;l+=2)c[l][0](c[l+1]);c.length=0}}}function r(a){s(a(e))}function i(a,u=d){const l=[a,u];return o.add(l),o.size===1&&(n=t(s)||d),a(e),()=>{o.delete(l),o.size===0&&(n(),n=null)}}return{set:s,update:r,subscribe:i}}let g="";function A(e){g=e}const I="sveltekit:scroll",y="sveltekit:index",b={tap:1,hover:2,viewport:3,eager:4,off:-1};function S(e){let t=e.baseURI;if(!t){const n=e.getElementsByTagName("base");t=n.length?n[0].href:e.URL}return t}function T(){return{x:pageXOffset,y:pageYOffset}}function f(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const h={...b,"":b.hover};function m(e){let t=e.assignedSlot??e.parentNode;return(t==null?void 0:t.nodeType)===11&&(t=t.host),t}function U(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=m(e)}}function L(e,t){let n;try{n=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI)}catch{}const o={rel_external:(e.getAttribute("rel")||"").split(/\s+/).includes("external"),download:e.hasAttribute("download"),target:!!(e instanceof SVGAElement?e.target.baseVal:e.target)},s=!n||R(n,t)||o.rel_external||o.target||o.download;return{url:n,has:o,external:s}}function O(e){let t=null,n=null,o=null,s=null,r=e;for(;r&&r!==document.documentElement;)n===null&&(n=f(r,"preload-code")),o===null&&(o=f(r,"preload-data")),t===null&&(t=f(r,"noscroll")),s===null&&(s=f(r,"reload")),r=m(r);return{preload_code:h[n??"off"],preload_data:h[o??"off"],noscroll:t==="off"?!1:t===""?!0:null,reload:s==="off"?!1:s===""?!0:null}}function _(e){const t=p(e);let n=!0;function o(){n=!0,t.update(i=>i)}function s(i){n=!1,t.set(i)}function r(i){let a;return t.subscribe(u=>{(a===void 0||n&&u!==a)&&i(a=u)})}return{notify:o,set:s,subscribe:r}}function v(){const{set:e,subscribe:t}=p(!1);let n;async function o(){clearTimeout(n);const s=await fetch(`${E}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(s.ok){const i=(await s.json()).version!==g;return i&&(e(!0),clearTimeout(n)),i}else throw new Error(`Version check failed: ${s.status}`)}return{subscribe:t,check:o}}function R(e,t){return e.origin!==location.origin||!e.pathname.startsWith(t)}function V(e){e.client}const N={url:_({}),page:_({}),navigating:p(null),updated:v()};export{y as I,b as P,I as S,L as a,O as b,T as c,V as d,A as e,U as f,S as g,R as i,N as s};

View file

@ -0,0 +1 @@
import{S,i as q,s as x,k as f,q as _,a as H,l as d,m as g,r as h,h as u,c as k,b as m,G as v,u as $,B as E,H as y}from"../chunks/index-bf58d8cc.js";import{s as B}from"../chunks/singletons-f54962ea.js";const C=()=>{const s=B;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},G={subscribe(s){return C().page.subscribe(s)}};function P(s){var b;let t,r=s[0].status+"",o,n,i,c=((b=s[0].error)==null?void 0:b.message)+"",l;return{c(){t=f("h1"),o=_(r),n=H(),i=f("p"),l=_(c)},l(e){t=d(e,"H1",{});var a=g(t);o=h(a,r),a.forEach(u),n=k(e),i=d(e,"P",{});var p=g(i);l=h(p,c),p.forEach(u)},m(e,a){m(e,t,a),v(t,o),m(e,n,a),m(e,i,a),v(i,l)},p(e,[a]){var p;a&1&&r!==(r=e[0].status+"")&&$(o,r),a&1&&c!==(c=((p=e[0].error)==null?void 0:p.message)+"")&&$(l,c)},i:E,o:E,d(e){e&&u(t),e&&u(n),e&&u(i)}}}function j(s,t,r){let o;return y(s,G,n=>r(0,o=n)),[o]}let A=class extends S{constructor(t){super(),q(this,t,j,P,x,{})}};export{A as default};

View file

@ -0,0 +1 @@
import{S as l,i,s as r,C as u,D as f,E as _,F as c,f as p,t as d}from"../../chunks/index-bf58d8cc.js";function m(n){let s;const o=n[1].default,e=u(o,n,n[0],null);return{c(){e&&e.c()},l(t){e&&e.l(t)},m(t,a){e&&e.m(t,a),s=!0},p(t,[a]){e&&e.p&&(!s||a&1)&&f(e,o,t,t[0],s?c(o,t[0],a,null):_(t[0]),null)},i(t){s||(p(e,t),s=!0)},o(t){d(e,t),s=!1},d(t){e&&e.d(t)}}}function $(n,s,o){let{$$slots:e={},$$scope:t}=s;return n.$$set=a=>{"$$scope"in a&&o(0,t=a.$$scope)},[t,e]}class h extends l{constructor(s){super(),i(this,s,$,m,r,{})}}export{h as default};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{p}from"../../chunks/_layout-da46b06b.js";export{p as prerender};

File diff suppressed because one or more lines are too long

1
docs/_app/version.json Normal file
View file

@ -0,0 +1 @@
{"version":"1673541144239"}

BIN
docs/babyblahaj-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
docs/babyblahaj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

1
docs/blobhaj.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Prior_to_November" data-name="Prior to November" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800"><defs><style>.cls-1{fill:none;}.cls-2{fill:#54899b;}.cls-3{clip-path:url(#clip-path);}.cls-4{fill:#e5e6e6;}.cls-5{fill:#2f152f;}</style><clipPath id="clip-path"><path class="cls-1" d="M35.77,707.08C5.14,258.38,642.84,29.74,705.47,362.41c7.51,80.81,38.77,240.76,44.43,295.84,1.35,13.11-7.93,22.36-17.68,31.22-126.13,110.2-591.67,55.36-696.45,17.61"/></clipPath></defs><g id="hold_asparagus" data-name="hold asparagus"><g id="Layer_1" data-name="Layer 1"><path class="cls-2" d="M40.87,704.18C10.24,255.49,647.94,26.85,710.57,359.52c7.51,80.81,38.77,240.75,44.43,295.83,1.35,13.12-7.93,22.36-17.68,31.23-126.13,110.2-591.67,55.35-696.45,17.6"/><g class="cls-3"><path class="cls-4" d="M604.33,773.11c169.33,123.07-333-21.41-542.28-21.41s73.59-603.91,406.82-510.25c20.84,5.86,65.57,33.76,72.2,94.16C547,389.23,525,455.33,517.92,516.13c-6.38,54.65,2.07,105.68,5.22,128.14C530.2,694.57,570.83,748.76,604.33,773.11Z"/></g><path class="cls-5" d="M631.06,474.87c-6.38-3.86-22.41,4-29.22,7.07-32.92,14.72-57,51.28-50.4,86.72a49.37,49.37,0,0,0,5.67,15.92,51.3,51.3,0,0,0,13.08,14.24c23.25,18,57.27,20.72,83.09,6.67s42-44.11,39.51-73.4c-1.54-18.13-10.11-36-24.64-46.9s-35.1-14.13-51.52-6.32"/><path class="cls-5" d="M176.14,382.69a457.37,457.37,0,0,1-4,58l.53-4c-.47,3.5-1.33,7-1,10.58a32.41,32.41,0,0,0,3.47,12.49,23.27,23.27,0,0,0,2.55,3.71,21.14,21.14,0,0,0,3,3.43,26.27,26.27,0,0,0,10.66,5.91,27.13,27.13,0,0,0,13-.1l3.86-.89,29.89-6.9c4.14-1,8.33-2.09,12.54-2.7l-4,.53a24.34,24.34,0,0,1,6-.11l-4-.54a16.81,16.81,0,0,1,3.89,1L249,461.64a26.2,26.2,0,0,1,5.52,3.36l-3-2.35c3.19,2.48,6.24,5.15,9.34,7.74l15.76,13.19c8.22,6.87,16.41,13.79,24.66,20.63,3.34,2.76,6.92,5.28,11.3,6.14a24.91,24.91,0,0,0,7,.63,26.87,26.87,0,0,0,10.53-2.91c4.53-2.21,8.22-6.31,10.71-10.61l5.28-9.13,21.33-36.87,15.11-26.14.26-.44a15.59,15.59,0,0,0,1.51-11.56,15.29,15.29,0,0,0-6.89-9c-3.49-1.84-7.7-2.77-11.56-1.52a15.6,15.6,0,0,0-9,6.9l-14.53,25.12-22.85,39.52-3.65,6.31a34,34,0,0,1-2.2,3.56l2.34-3a13.54,13.54,0,0,1-2.15,2.13l3-2.34a12.29,12.29,0,0,1-2.19,1.28l3.58-1.51a11.4,11.4,0,0,1-2.67.74l4-.54a11.15,11.15,0,0,1-2.45,0l4,.54a12,12,0,0,1-2.42-.64l3.58,1.51a11.83,11.83,0,0,1-2.57-1.5l3,2.34c-4.82-3.91-9.53-8-14.29-11.94L284.27,451l-7.41-6.19-1.41-1.18a77,77,0,0,0-8.47-6.46,32.86,32.86,0,0,0-4.63-2.23,23.43,23.43,0,0,0-5.23-1.73,38.54,38.54,0,0,0-7.18-.75,58,58,0,0,0-7.68.87c-1.87.29-3.71.7-5.54,1.12l-2.47.57-12.87,3c-8.39,1.94-16.78,3.9-25.17,5.81-.43.1-.86.19-1.3.26l4-.54a11,11,0,0,1-2.5,0l4,.53a10.86,10.86,0,0,1-2.62-.73l3.58,1.51a12.19,12.19,0,0,1-2.09-1.23l3,2.34a12.84,12.84,0,0,1-2.22-2.2l2.34,3a14.12,14.12,0,0,1-1.79-3.11l1.51,3.59a15.31,15.31,0,0,1-1-3.66l.53,4a16.39,16.39,0,0,1,0-4l-.54,4a484.59,484.59,0,0,0,4.94-64.91,15.5,15.5,0,0,0-4.4-10.61,15.23,15.23,0,0,0-10.6-4.39c-3.88.17-7.91,1.45-10.61,4.39a15.68,15.68,0,0,0-4.39,10.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
docs/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
docs/hero.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

26
docs/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/blahaj/favicon.png" />
<meta name="viewport" content="width=device-width" />
<meta http-equiv="content-security-policy" content="">
<link rel="modulepreload" href="/blahaj/_app/immutable/start-0331b724.js">
<link rel="modulepreload" href="/blahaj/_app/immutable/chunks/index-bf58d8cc.js">
<link rel="modulepreload" href="/blahaj/_app/immutable/chunks/singletons-f54962ea.js">
<link rel="modulepreload" href="/blahaj/_app/immutable/chunks/paths-b4419565.js">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script type="module" data-sveltekit-hydrate="45h">
import { start } from "/blahaj/_app/immutable/start-0331b724.js";
start({
env: {},
paths: {"base":"/blahaj","assets":"/blahaj"},
target: document.querySelector('[data-sveltekit-hydrate="45h"]').parentNode,
version: "1673541144239"
});
</script></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
docs/originalblahaj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

99
docs/vite-manifest.json Normal file
View file

@ -0,0 +1,99 @@
{
"src/routes/+layout.css": {
"file": "_app/immutable/assets/_layout-2fe00a85.css",
"src": "src/routes/+layout.css"
},
"_paths-b4419565.js": {
"file": "_app/immutable/chunks/paths-b4419565.js"
},
"__layout-da46b06b.js": {
"file": "_app/immutable/chunks/_layout-da46b06b.js"
},
"_singletons-f54962ea.js": {
"file": "_app/immutable/chunks/singletons-f54962ea.js",
"imports": [
"_index-bf58d8cc.js",
"_paths-b4419565.js"
]
},
".svelte-kit/generated/nodes/1.js": {
"file": "_app/immutable/chunks/1-01438cb8.js",
"src": ".svelte-kit/generated/nodes/1.js",
"isDynamicEntry": true,
"imports": [
"node_modules/@sveltejs/kit/src/runtime/components/error.svelte"
]
},
"node_modules/@sveltejs/kit/src/runtime/components/error.svelte": {
"file": "_app/immutable/components/error.svelte-0ab2467a.js",
"src": "node_modules/@sveltejs/kit/src/runtime/components/error.svelte",
"isEntry": true,
"imports": [
"_index-bf58d8cc.js",
"_singletons-f54962ea.js"
]
},
"src/routes/+layout.js": {
"file": "_app/immutable/modules/pages/_layout.js-9cbb603b.js",
"src": "src/routes/+layout.js",
"isEntry": true,
"imports": [
"__layout-da46b06b.js"
]
},
".svelte-kit/generated/nodes/0.js": {
"file": "_app/immutable/chunks/0-7a2c0d3b.js",
"src": ".svelte-kit/generated/nodes/0.js",
"isDynamicEntry": true,
"imports": [
"__layout-da46b06b.js",
"src/routes/+layout.svelte"
]
},
".svelte-kit/generated/nodes/2.js": {
"file": "_app/immutable/chunks/2-688a5c8c.js",
"src": ".svelte-kit/generated/nodes/2.js",
"isDynamicEntry": true,
"imports": [
"src/routes/+page.svelte"
]
},
"src/routes/+layout.svelte": {
"file": "_app/immutable/components/pages/_layout.svelte-76471106.js",
"src": "src/routes/+layout.svelte",
"isEntry": true,
"imports": [
"_index-bf58d8cc.js"
],
"css": [
"_app/immutable/assets/_layout-2fe00a85.css"
]
},
"_index-bf58d8cc.js": {
"file": "_app/immutable/chunks/index-bf58d8cc.js"
},
"node_modules/@sveltejs/kit/src/runtime/client/start.js": {
"file": "_app/immutable/start-0331b724.js",
"src": "node_modules/@sveltejs/kit/src/runtime/client/start.js",
"isEntry": true,
"imports": [
"_index-bf58d8cc.js",
"_singletons-f54962ea.js",
"_paths-b4419565.js"
],
"dynamicImports": [
".svelte-kit/generated/nodes/0.js",
".svelte-kit/generated/nodes/1.js",
".svelte-kit/generated/nodes/2.js"
]
},
"src/routes/+page.svelte": {
"file": "_app/immutable/components/pages/_page.svelte-a3469a4f.js",
"src": "src/routes/+page.svelte",
"isEntry": true,
"imports": [
"_index-bf58d8cc.js",
"_paths-b4419565.js"
]
}
}

694
get-store-data.js Normal file
View file

@ -0,0 +1,694 @@
import fs from 'fs'
import JSON6 from 'json-6'
let ikeaData = [
{
name: "United Kingdom",
abbrv: "the UK",
emoji: '🇬🇧',
countryCode: 'gb',
urlCode: "gb/en",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "United States",
abbrv: "the US",
emoji: '🇺🇸',
countryCode: 'us',
urlCode: "us/en",
itemIds: {
original: "90373590",
baby: "70540665"
}
},
{
name: "Canada",
emoji: '🇨🇦',
countryCode: 'ca',
urlCode: "ca/en",
itemIds: {
original: "90373590",
baby: "70540665"
}
},
{
name: "France",
emoji: '🇫🇷',
countryCode: 'fr',
urlCode: "fr/fr",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Sweden",
emoji: '🇸🇪',
countryCode: 'se',
urlCode: "se/sv",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Germany",
emoji: '🇩🇪',
countryCode: 'de',
urlCode: "de/de",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Switzerland",
emoji: '🇨🇭',
countryCode: 'ch',
urlCode: "ch/en",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Austria",
emoji: '🇦🇹',
countryCode: 'at',
urlCode: "at/de",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Belgium",
emoji: '🇧🇪',
countryCode: 'be',
urlCode: "be/en",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Norway",
emoji: '🇳🇴',
countryCode: 'no',
urlCode: "no/no",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Italy",
emoji: '🇮🇹',
countryCode: 'it',
urlCode: "it/it",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Netherlands",
emoji: '🇳🇱',
countryCode: 'nl',
urlCode: "nl/en",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Poland",
emoji: '🇵🇱',
countryCode: 'pl',
urlCode: "pl/pl",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "Spain",
emoji: '🇪🇸',
countryCode: 'es',
urlCode: "es/es",
itemIds: {
baby: "20540663"
}
},
{
name: "Israel",
emoji: '🇮🇱',
countryCode: 'il',
urlCode: "il/he",
itemIds: {
original: "30373588",
baby: "20540663"
}
},
{
name: "India",
emoji: '🇮🇳',
countryCode: 'in',
urlCode: "in/en",
itemIds: {
original: "10373589"
}
},
{
name: "Japan",
emoji: '🇯🇵',
countryCode: 'jp',
urlCode: "jp/ja",
itemIds: {
original: "10373589",
baby: '00540664'
}
},
{
name: "South Korea",
emoji: '🇰🇷',
countryCode: 'kr',
urlCode: "kr/en",
itemIds: {
original: "10373589",
baby: '00540664'
}
},
{
name: "Australia",
emoji: '🇦🇺',
countryCode: 'au',
urlCode: "au/en",
itemIds: {
original: "10373589",
baby: '00540664'
}
},
{
name: "Denmark",
emoji: '🇩🇰',
countryCode: 'dk',
urlCode: "dk/da",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Finland",
emoji: '🇫🇮',
countryCode: 'fi',
urlCode: "fi/fi",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Portugal",
emoji: '🇵🇹',
countryCode: 'pt',
urlCode: "pt/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Saudi Arabia",
emoji: '🇸🇦',
countryCode: 'sa',
urlCode: "sa/en",
itemIds: {
original: '80556378',
baby: '30540667'
}
},
{
name: "Czech Republic",
emoji: '🇨🇿',
countryCode: 'cz',
urlCode: "cz/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "United Arab Emirates",
emoji: '🇦🇪',
countryCode: 'ae',
urlCode: "ae/en",
itemIds: {
original: '30373588',
baby: '30540667'
}
},
{
name: "Malaysia",
emoji: '🇲🇾',
countryCode: 'my',
urlCode: "my/en",
itemIds: {
original: "10373589",
baby: "00540664"
}
},
{
name: "Thailand",
emoji: '🇹🇭',
countryCode: 'th',
urlCode: "th/en",
itemIds: {
original: "10373589",
baby: "00540664"
}
},
{
name: "Macau",
emoji: '🇲🇴',
countryCode: 'ma',
urlCode: "ma/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Egypt",
emoji: '🇪🇬',
countryCode: 'eg',
urlCode: "eg/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Qatar",
emoji: '🇶🇦',
countryCode: 'qa',
urlCode: "qa/en",
itemIds: {
baby: '30540667'
}
},
{
name: "Jordan",
emoji: '🇯🇴',
countryCode: 'jo',
urlCode: "jo/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Croatia",
emoji: '🇭🇷',
countryCode: 'hr',
urlCode: "hr/hr",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Singapore",
emoji: '🇸🇬',
countryCode: 'sg',
urlCode: "sg/en",
itemIds: {
original: '10373589',
baby: '00540664'
}
},
{
name: "Kuwait",
emoji: '🇰🇼',
countryCode: 'kw',
urlCode: "kw/en",
itemIds: {
original: '80556378',
baby: '30540667'
}
},
{
name: "Hungary",
emoji: '🇭🇺',
countryCode: 'hu',
urlCode: "hu/hu",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Romania",
emoji: '🇷🇴',
countryCode: 'ro',
urlCode: "ro/ro",
itemIds: {
baby: '20540663'
}
},
{
name: "Ireland",
emoji: '🇮🇪',
countryCode: 'ie',
urlCode: "ie/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Morocco",
emoji: '🇲🇦',
countryCode: 'ma',
urlCode: "ma/en",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Mexico",
emoji: '🇲🇽',
countryCode: 'mx',
urlCode: "mx/en",
itemIds: {
original: '90373590',
baby: '70540665'
}
},
{
name: "Serbia",
emoji: '🇷🇸',
countryCode: 'rs',
urlCode: "rs/sr",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Slovakia",
emoji: '🇸🇰',
countryCode: 'sk',
urlCode: "sk/sk",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Bahrain",
emoji: '🇧🇭',
countryCode: 'bh',
urlCode: "bh/en",
itemIds: {
baby: '30540667'
}
},
{
name: "Slovenia",
emoji: '🇸🇮',
countryCode: 'si',
urlCode: "si/sl",
itemIds: {
original: '30373588',
baby: '20540663'
}
},
{
name: "Philippines",
emoji: '🇵🇭',
countryCode: 'ph',
urlCode: "ph/en",
itemIds: {
original: '10373589',
baby: '00540664'
}
},
{
name: "Oman",
emoji: '🇴🇲',
countryCode: 'om',
urlCode: "om/en",
itemIds: {
original: '80556378',
baby: '30540667'
}
},
{
name: "Chile",
emoji: '🇨🇱',
countryCode: 'cl',
urlCode: "cl/es",
unavailable: true,
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Chile seems to not sell BLÅHAJ."
},
{
name: "Ukraine",
emoji: '🇺🇦',
countryCode: 'ua',
urlCode: "ua/uk",
unavailable: true,
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Ukraine seems to not sell BLÅHAJ."
},
{
name: "Lithuania",
emoji: '🇱🇹',
itemUrls: {
original: 'https://www.ikea.lt/en/products/children-s-room/for-babies/comfort-toys/blahaj-soft-toy-art-30373588',
baby: 'https://www.ikea.lt/en/products/children-s-room/for-babies/comfort-toys/blahaj-soft-toy-art-20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Lithuania's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Puerto Rico",
emoji: '🇵🇷',
itemUrls: {
original: 'https://www.ikea.pr/puertorico/en/pd/blahaj-soft-toy-art-90373590',
baby: 'https://www.ikea.pr/puertorico/en/pd/blahaj-soft-toy-art-70540665'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Puerto Rico's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Iceland",
emoji: '🇮🇸',
itemUrls: {
original: 'https://www.ikea.is/en/products/baby-children/play/soft-toys/blahaj-soft-toy-art-30373588',
baby: 'https://www.ikea.is/en/products/christmas/gift-ideas/gift-ideas/blahaj-soft-toy-art-20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Iceland's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Cyprus",
emoji: '🇨🇾',
itemUrls: {
baby: 'https://www.ikea.com.cy/en/products/baby-children/baby/baby-toys/12-months/blahaj-soft-toy-baby-shark-55-cm/20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Cyprus's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Latvia",
emoji: '🇱🇻',
itemUrls: {
original: 'https://www.ikea.lv/en/products/children-s-room/children-3-7/comfort-toys/blahaj-soft-toy-art-30373588',
baby: 'https://www.ikea.lv/en/products/children-s-room/children-3-7/comfort-toys/blahaj-soft-toy-art-20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Latvia's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Estonia",
emoji: '🇪🇪',
itemUrls: {
original: 'https://www.ikea.ee/en/products/children-s-room/baby/comfort-toys/blahaj-soft-toy-art-30373588',
baby: 'https://www.ikea.ee/en/products/children-s-room/baby/comfort-toys/blahaj-soft-toy-art-20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "PLACEHOLDER's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Turkey",
emoji: '🇹🇷',
itemUrls: {
original: 'https://www.ikea.com.tr/en/p30373588',
baby: 'https://www.ikea.com.tr/en/p20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Turkey's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Dominican Republic",
emoji: '🇩🇴',
itemUrls: {
original: 'https://www.ikea.com.do/en/pd/blahaj-soft-toy-art-90373590',
baby: 'https://www.ikea.com.do/en/pd/blahaj-soft-toy-art-70540665'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "The Dominican Republic's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Bulgaria",
emoji: '🇧🇬',
itemUrls: {
baby: 'https://www.ikea.bg/products/baby-children/children-3-7/toys-play/soft-toys/blahaj-plyushena-igrachka-malka-akula-55-sm/20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Bulgaria's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Taiwan",
emoji: '🇹🇼',
itemUrls: {
original: 'https://www.ikea.com.tw/en/products/childrens-play/comfort-toys/blahaj-art-10373589',
baby: 'https://www.ikea.com.tw/en/products/childrens-play/comfort-toys/blahaj-art-00540664'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Taiwan's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Indonesia",
emoji: '🇮🇩',
itemUrls: {
original: 'https://www.ikea.co.id/en/products/children-play/comfort-toys/blahaj-art-10373589',
baby: 'https://www.ikea.co.id/en/products/children-play/comfort-toys/blahaj-art-00540664'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Indonesia's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Greece",
emoji: '🇬🇷',
itemUrls: {
baby: 'https://www.ikea.gr/en/products/baby-children/baby/baby-toys/12-months/blahaj-soft-toy-baby-shark-55-cm/20540663'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Greece's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "Hong Kong",
emoji: '🇭🇰',
itemUrls: {
original: 'https://www.ikea.com.hk/en/products/childrens-toys-play-and-learn/soft-toys/blahaj-art-10373589',
baby: 'https://www.ikea.com.hk/en/products/childrens-toys-play-and-learn/soft-toys/blahaj-art-00540664'
},
cantCheckUrls: true,
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "Hong Kong's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
},
{
name: "China",
emoji: '🇨🇳',
countryCode: 'cn',
urlCode: "cn/zh",
itemIds: {
original: "10373589",
baby: "00540664"
},
cantCheckAutomatically: true,
cantCheckAutomaticallyMessage: "China's IKEA website uses a different codebase, so this site cannot check the BLÅHAJ stock automatically at the moment. Check on their site at the link below!"
}
]
async function getProperUrl(url) {
let html = await(await fetch(url)).text()
return html.match(/<meta (?:[^<>]*?)?property="og:url" content="(.*?)"\/?>/)[1]
}
async function getStoresByCountry(countryData) {
if (countryData.cantCheckUrls) return countryData
const itemPageHtml = await (await fetch(`https://www.ikea.com/${countryData.urlCode}/p/blahaj-${countryData.itemIds.original??countryData.itemIds.baby}/`)).text()
let firstItemUrl = itemPageHtml.match(/<meta (?:[^<>]*?)?property="og:url" content="(.*?)"\/?>/)[1]
let itemUrls = {}
if (countryData.itemIds.original) {
itemUrls.original = firstItemUrl
if (countryData.itemIds.baby) itemUrls.baby = await getProperUrl(`https://www.ikea.com/${countryData.urlCode}/p/blahaj-${countryData.itemIds.baby}/`)
} else {
itemUrls.baby = firstItemUrl
}
if (countryData.cantCheckAutomatically) {
return {
...countryData,
itemUrls
}
}
let mainJsUrl = `https://www.ikea.com/${countryData.urlCode}/products/javascripts/${itemPageHtml.match(/\/products\/javascripts\/(range-pip-main\..*?)">/)[1]}`
let pipMainJs = await (await fetch(mainJsUrl)).text()
let matches = pipMainJs.match(/javascripts\/"\+\(({.*?})\[e\][^{]*?({.*?})/m)
let jsFileNames = JSON6.parse(matches[1])
let jsFileHashes = JSON6.parse(matches[2])
let jsFiles = []
for (let i of Object.keys(jsFileNames)) {
jsFiles.push(`${jsFileNames[i]}.${jsFileHashes[i]}.js`)
}
let stockcheckFilename = jsFiles.find(e => e.startsWith('range-stockcheck.'))
if (!stockcheckFilename) throw new Error('Found no stockcheck JS file.')
let stockcheckJs = await (await fetch(`https://www.ikea.com/${countryData.urlCode}/products/javascripts/${stockcheckFilename}`)).text()
let allStoresJson = stockcheckJs.match(/allStores=(\[.*?\])/)[1]
let stores = JSON6.parse(allStoresJson).map(e => {
return {
value: e.value,
name: e.name,
address: e.storeAddress.address,
displayAddress: e.storeAddress.displayAddress
}
})
return {
...countryData,
itemUrls,
stores
}
}
async function start() {
await Promise.all(ikeaData.map((e, i) => {
return (async () => {
ikeaData[i] = await getStoresByCountry(ikeaData[i])
})()
}))
ikeaData.sort((a, b) => a.name.localeCompare(b.name))
let storeDataJs = `export default ${JSON.stringify(ikeaData)}`
await fs.promises.writeFile('src/lib/store-data.js', storeDataJs)
}
start()

View file

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="rainbow">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/things/style.css">
<link rel="stylesheet" href="style.css">
<script src="utf8.js"></script>
<title>BLÅHAJ Radar</title>
<meta name="title" content="BLÅHAJ Radar">
<meta name="description" content="Locate IKEA locations that have BLÅHAJ stock">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://hazy.gay/blahaj">
<meta property="og:title" content="BLÅHAJ Radar">
<meta property="og:description" content="Locate IKEA locations that have BLÅHAJ stock">
<meta property="og:image" content="https://hazy.gay/blahaj/thumb.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://hazy.gay/blahaj">
<meta property="twitter:site" content="@hazycora">
<meta property="twitter:creator" content="@hazycora">
<meta property="twitter:title" content="BLÅHAJ Radar">
<meta property="twitter:description" content="Locate IKEA locations that have BLÅHAJ stock">
<meta property="twitter:image" content="https://hazy.gay/blahaj/thumb.png">
</head>
<body>
<h1>BLÅHAJ Radar</h1>
<h3>View BLÅHAJ listings in various countries</h3>
<img id="cover" src="blahaj.png" alt="BLÅHAJ">
<noscript>This site requires JavaScript to fetch the BLÅHAJ listings. Sorry!</noscript>
<div id="error" style="display: none">
<h3>Error!</h3>
<p id="error-message"></p>
</div>
<div id="listings"></div>
<br>
<p id="share"><a href="https://twitter.com/intent/tweet?text=Locate%20IKEA%20locations%20that%20have%20BL%C3%85HAJ%20in%20stock&url=https%3A%2F%2Fhazy.gay%2Fblahaj&related=hazycora">share with twitter</a></p>
<br>
<p id="github"><a href="https://github.com/hazycora/blahaj">view source</a></p>
<br>
<p id="credits"><a href="/things">hazy.gay</a> - inspired by <a href="//git.lavender.software/charlotte/shark-radar">shark-radar</a></p>
<script src="app.js"></script>
</body>
</html>

1463
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "blahaj",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "node get-store-data.js && vite build",
"update-stores": "node get-store-data.js",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.1",
"@sveltejs/kit": "^1.0.0",
"autoprefixer": "^10.4.7",
"json-6": "^1.1.4",
"json-z": "^3.3.2",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",
"svelte": "^3.54.0",
"svelte-preprocess": "^4.10.7",
"vite": "^4.0.0"
},
"type": "module"
}

7
postcss.config.cjs Normal file
View file

@ -0,0 +1,7 @@
const autoprefixer = require("autoprefixer");
const config = {
plugins: [autoprefixer],
};
module.exports = config;

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

310
src/app.postcss Normal file
View file

@ -0,0 +1,310 @@
*, *::before, *::after {
box-sizing: border-box;
}
:root {
--bg: #180e2c;
--fg: white;
--accent-clr: #a87dff;
--link: #a87dff;
--font-family: 'Shippori Antique B1', sans-serif;
}
body {
overflow-x: hidden;
margin: 0;
background-color: var(--bg);
color: var(--fg);
font-family: var(--font-family);
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
p {
margin: 0;
}
a {
position: relative;
text-decoration: none;
color: var(--link);
}
a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: currentColor;
opacity: 0;
transform-origin: left center;
transform: scaleX(0);
transition: transform 300ms, opacity 300ms;
transition-delay: 50ms;
}
a:hover::after {
opacity: 1;
transform: scaleX(1);
}
img {
max-width: 100%;
}
ol, ul {
padding: 0;
margin: 0;
}
button {
display: block;
background: none;
color: inherit;
border: none;
margin: 0;
padding: 0;
font: inherit;
cursor: pointer;
}
.layout {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
min-height: 100svh;
}
.hero {
width: 100%;
min-height: 16rem;
position: relative;
display: flex;
padding: 2rem;
gap: 1rem;
justify-content: center;
align-items: center;
background-color: rgb(0 0 0 / 0.2);
}
.hero::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background-image: linear-gradient(90deg, #130b23a8 300px, #130b2338 600px);
}
.hero-message {
z-index: 2;
}
.hero-cover {
user-select: none;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 100%;
max-width: 800px;
object-fit: cover;
object-position: left 60%;
mask-image: linear-gradient(100deg, rgba(0, 0, 0, 0) 10%, rgba(0, 0, 0, 0.01) 15%, rgba(0, 0, 0, 0.05) 20%, rgba(0, 0, 0, 0.5) 60%, white 100%);
}
.blahaj-select-buttons {
display: flex;
width: 100%;
gap: 1rem;
}
.blahaj-select-buttons button {
position: relative;
flex-grow: 1;
padding: 0.4rem;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.blahaj-select-buttons button::after {
content: '';
background-color: var(--accent-clr);
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
opacity: 0;
transition: opacity 200ms;
}
.blahaj-select-buttons button[selected]::after {
opacity: 1;
}
.display-image-wrapper {
display: grid;
width: 100%;
height: 10rem;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
position: relative;
overflow: hidden;
user-select: none;
}
.display-image {
grid-column: 1/2;
grid-row: 1/2;
position: relative;
width: fit-content;
height: 100%;
margin: 0 auto;
}
.display-image img {
width: 100%;
height: 100%;
object-fit: contain;
}
.display-image-badge {
position: absolute;
right: 0.2rem;
top: 1.2rem;
transform: rotate(8deg);
padding: 0.2rem 0.4rem;
background-color: var(--accent-clr);
border-radius: 5px;
box-shadow: 0 1px 10px 12px rgba(0, 0, 0, 0.5);
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.country-filter-search {
background-color: rgba(0, 0, 0, 0.2);
border: none;
padding: 0.8rem 0.4rem;
color: inherit;
font: inherit;
border-radius: 6px;
}
.listings-wrapper {
display: grid;
overflow: hidden;
}
.listings {
grid-column: 1/2;
grid-row: 1/2;
display: flex;
flex-direction: column;
gap: 1rem;
}
.country-listings {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
padding: 0.5rem;
overflow: hidden;
}
.country-listings-emoji {
position: relative;
padding-right: 1.25rem;
}
.country-listings-emoji::after {
z-index: 15;
background-image: url(/blobhaj.svg);
content: '';
display: block;
position: absolute;
right: 0px;
width: 1.5rem;
top: 0px;
height: 2rem;
background-size: 100%;
background-repeat: no-repeat;
background-position: center bottom;
}
.country-listings-open-button {
width: 100%;
display: flex;
align-items: center;
}
.country-listings-open-button h2 {
text-align: left;
width: fit-content;
margin: 0;
}
.country-listings-open-button h2::after {
display: inline-block;
margin-left: 0.5rem;
content: '↴';
transition: transform 200ms;
transition-delay: 50ms;
transition-timing-function: cubic-bezier(.61,.76,.34,1.73);
}
.country-listings-open-button:hover h2::after {
transform: translateX(4px);
}
.country-listings-content {
margin-top: 1rem;
}
.listing {
display: block;
background: rgb(0 0 0 / 0.2);
border-radius: 0.5rem;
padding: 0.25rem;
margin: 1rem 0;
}
.listing.positive h3 {
color: #4eff93;
}
.listing.negative h3 {
color: #c83867;
}
.address {
font-size: 0.8rem;
opacity: 0.5;
}
.links {
background-color: rgb(0 0 0 / 0.2);
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
margin-top: 1rem;
padding: 1rem;
text-align: center;
}

View file

@ -0,0 +1,108 @@
<script>
import { slide } from 'svelte/transition'
import Listing from '$lib/Listing.svelte'
export let countryData
export let itemType
let listings
let loaded = false
let isOpen = false
function parseStock(availability) {
let store = countryData.stores.find(e => {
return e.value === availability.classUnitKey.classUnitCode
})
if (!store) {
return {
code: availability.classUnitKey.classUnitCode,
err: true,
store: {
name: 'unknown'
},
quantity: 0
}
}
let nextRestock = undefined
if (availability.buyingOption?.cashCarry?.availability?.restocks) {
nextRestock = availability.buyingOption.cashCarry.availability.restocks[0]
}
let quantity = availability.buyingOption?.cashCarry?.availability?.quantity??null
if (quantity===null) {
return {
code: availability.classUnitKey.classUnitCode,
store: store,
message: 'unavailable',
quantity: 0,
nextRestock: nextRestock
}
}
return {
code: availability.classUnitKey.classUnitCode,
store: store,
quantity: quantity,
nextRestock: nextRestock
}
}
async function fetchIkeaApi(path) {
let req
try {
req = await fetch(`https://api.ingka.ikea.com/${path}`, {
headers: {
'Accept': 'application/json;version=2',
'X-Client-ID': 'b6c117e5-ae61-4ef5-b4cc-e0b1e37f0631'
}
})
return await req.json()
} catch (error) {
throw error
}
}
async function loadContent() {
if (countryData.cantCheckAutomatically) return
let listingData = await fetchIkeaApi(`cia/availabilities/ru/${countryData.countryCode}?itemNos=${countryData.itemIds[itemType]}&expand=StoresList,Restocks,SalesLocations`)
let stocks = listingData.availabilities.map(e => parseStock(e))
return stocks.sort((a, b) => {
return b.quantity - a.quantity;
}).filter(e => !e.err)
}
function onClick() {
isOpen=!isOpen
if (isOpen&&!loaded) {
listings = loadContent()
loaded = true
}
}
</script>
<div class="country-listings" class:baby-blahaj={itemType==='baby'}>
<button class="country-listings-open-button" on:click={onClick}>
<h2 class="country-listings-name-wrapper">
<span class="country-listings-emoji" aria-hidden="true">{countryData.emoji}</span>
<span>{countryData.name}</span>
</h2>
</button>
{#if isOpen}
<div class="country-listings-content" out:slide={{duration: 100}}>
{#if countryData.cantCheckAutomatically}
<p>{countryData.cantCheckAutomaticallyMessage}</p>
<a href="{countryData.itemUrls[itemType]}">See the listings for BLÅHAJ in {countryData.abbrv??countryData.name}</a>
{:else}
<a href="{countryData.itemUrls[itemType]}">See the listings for BLÅHAJ in {countryData.abbrv??countryData.name}</a>
{#if loaded}
{#await listings}
<p>loading...</p>
{:then listings}
<ul>
{#each listings as listing, i}
<Listing {listing} delay={(400/listings.length)*i}></Listing>
{/each}
</ul>
{/await}
{/if}
{/if}
</div>
{/if}
</div>

35
src/lib/Listing.svelte Normal file
View file

@ -0,0 +1,35 @@
<script>
import { fly } from 'svelte/transition'
export let listing
export let delay = 0
let positive = listing.quantity&&listing.quantity > 0
let restockRange
let message = listing.message
if (!message) {
if (listing.quantity===1) {
message = `${listing.quantity} shark`
} else {
message = `${listing.quantity} sharks`
}
}
if (listing.nextRestock) {
let dateEarliest = formatDate(listing.nextRestock.earliestDate)
let dateLatest = formatDate(listing.nextRestock.latestDate)
restockRange = (dateEarliest === dateLatest) ? dateEarliest : `${dateEarliest}-${dateLatest}`
}
function formatDate(str) {
let date = new Date(str)
return new Intl.DateTimeFormat('en-US').format(date)
}
</script>
<div in:fly={{delay, x: -100, duration: 300}} out:fly={{delay, 0: -100, duration: 300}} class="listing" class:positive={positive} class:negative={!positive}>
<h3>{listing.store.name}</h3>
<p class="address">{listing.store.address}</p>
<p class="quantity">{message}</p>
{#if listing.nextRestock}
<p class="restock-date">Restocking {listing.nextRestock.quantity} sharks on {restockRange}</p>
{/if}
</div>

1
src/lib/store-data.js Normal file

File diff suppressed because one or more lines are too long

1
src/routes/+layout.js Normal file
View file

@ -0,0 +1 @@
export const prerender = true

View file

@ -0,0 +1,5 @@
<script>
import "../app.postcss";
</script>
<slot />

99
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,99 @@
<script>
import { base } from '$app/paths'
import { fly } from 'svelte/transition'
import CountryListings from '$lib/CountryListings.svelte'
import ikeaData from '$lib/store-data.js'
let blahajType = 'original'
let countryFilter = ''
let filteredCountries = ikeaData
function setType(newType) {
blahajType = newType
}
$: {
countryFilter = countryFilter
filteredCountries = ikeaData.map(filterCountry).sort((a, b) => b.points-a.points).filter(e => e.points>=10)
}
function filterCountry(countryData) {
if (countryFilter=='') return {
...countryData,
points: 10
}
let countryFilterKeywords = countryFilter.toLowerCase().split(/\s/gm)
let points = 0
for (let keyword of countryFilterKeywords) {
let foundKeyword = false
if (countryData.name.toLowerCase().includes(keyword)) {
points += 20
foundKeyword = true
}
if (countryData.stores?.find(e => e.name.toLowerCase().includes(keyword))) {
points += 10
foundKeyword = true
}
if (countryData.stores?.find(e => e.address.toLowerCase().includes(keyword))) {
points += 5
foundKeyword = true
}
if (!foundKeyword) points = -99999
}
return {
...countryData,
points
}
}
</script>
<div class="layout">
<div class="hero">
<div class="hero-message">
<h1>BLÅHAJ Radar</h1>
<p>View BLÅHAJ listings in various countries</p>
</div>
<img class="hero-cover" src="{base}/hero.jpg" alt="BLÅHAJ">
</div>
<div class="main-content">
<div class="blahaj-select-buttons">
<button on:click={e => setType('original')} selected={blahajType==='original'||null}>Original BLÅHAJ</button>
<button on:click={e => setType('baby')} selected={blahajType==='baby'||null}>Baby BLÅHAJ</button>
</div>
<div class="display-image-wrapper">
{#if blahajType==='original'}
<div transition:fly={{x: -800, duration: 400}} class="display-image">
<img src="{base}/originalblahaj.webp" alt="">
</div>
{:else if blahajType==='baby'}
<div transition:fly={{x: 800, duration: 400}} class="display-image">
<img src="{base}/babyblahaj.webp" alt="">
<div class="display-image-badge">NEW!</div>
</div>
{/if}
</div>
<input type="text" class="country-filter-search" bind:value={countryFilter} placeholder="Search" autocomplete="off">
<div class="listings-wrapper">
{#if blahajType==='original'}
<div class="listings" transition:fly={{x: -800, duration: 400}}>
{#each filteredCountries.filter(e => e.itemIds?.original) as countryData}
<CountryListings {countryData} itemType='original'></CountryListings>
{/each}
</div>
{:else if blahajType==='baby'}
<div class="listings" transition:fly={{x: 800, duration: 400}}>
{#each filteredCountries.filter(e => e.itemIds?.baby) as countryData}
<CountryListings {countryData} itemType='baby'></CountryListings>
{/each}
</div>
{/if}
</div>
</div>
<div class="links">
<p><a href="https://twitter.com/intent/tweet?text=Locate%20IKEA%20locations%20that%20have%20BL%C3%85HAJ%20in%20stock&url=https%3A%2F%2Fhazy.gay%2Fblahaj&related=hazycora">share with twitter</a></p>
<p><a href="https://github.com/hazycora/blahaj">view source</a></p>
<p><a href="http://heatherhorns.com/emoji/">blobhaj emoji by heatherhorns</a></p>
<p><a href="https://hazy.gay/things">hazy.gay</a> - inspired by <a href="//git.lavender.software/charlotte/shark-radar">shark-radar</a></p>
</div>
</div>

0
static/.nojekyll Normal file
View file

BIN
static/babyblahaj-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
static/babyblahaj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

1
static/blobhaj.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Prior_to_November" data-name="Prior to November" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800"><defs><style>.cls-1{fill:none;}.cls-2{fill:#54899b;}.cls-3{clip-path:url(#clip-path);}.cls-4{fill:#e5e6e6;}.cls-5{fill:#2f152f;}</style><clipPath id="clip-path"><path class="cls-1" d="M35.77,707.08C5.14,258.38,642.84,29.74,705.47,362.41c7.51,80.81,38.77,240.76,44.43,295.84,1.35,13.11-7.93,22.36-17.68,31.22-126.13,110.2-591.67,55.36-696.45,17.61"/></clipPath></defs><g id="hold_asparagus" data-name="hold asparagus"><g id="Layer_1" data-name="Layer 1"><path class="cls-2" d="M40.87,704.18C10.24,255.49,647.94,26.85,710.57,359.52c7.51,80.81,38.77,240.75,44.43,295.83,1.35,13.12-7.93,22.36-17.68,31.23-126.13,110.2-591.67,55.35-696.45,17.6"/><g class="cls-3"><path class="cls-4" d="M604.33,773.11c169.33,123.07-333-21.41-542.28-21.41s73.59-603.91,406.82-510.25c20.84,5.86,65.57,33.76,72.2,94.16C547,389.23,525,455.33,517.92,516.13c-6.38,54.65,2.07,105.68,5.22,128.14C530.2,694.57,570.83,748.76,604.33,773.11Z"/></g><path class="cls-5" d="M631.06,474.87c-6.38-3.86-22.41,4-29.22,7.07-32.92,14.72-57,51.28-50.4,86.72a49.37,49.37,0,0,0,5.67,15.92,51.3,51.3,0,0,0,13.08,14.24c23.25,18,57.27,20.72,83.09,6.67s42-44.11,39.51-73.4c-1.54-18.13-10.11-36-24.64-46.9s-35.1-14.13-51.52-6.32"/><path class="cls-5" d="M176.14,382.69a457.37,457.37,0,0,1-4,58l.53-4c-.47,3.5-1.33,7-1,10.58a32.41,32.41,0,0,0,3.47,12.49,23.27,23.27,0,0,0,2.55,3.71,21.14,21.14,0,0,0,3,3.43,26.27,26.27,0,0,0,10.66,5.91,27.13,27.13,0,0,0,13-.1l3.86-.89,29.89-6.9c4.14-1,8.33-2.09,12.54-2.7l-4,.53a24.34,24.34,0,0,1,6-.11l-4-.54a16.81,16.81,0,0,1,3.89,1L249,461.64a26.2,26.2,0,0,1,5.52,3.36l-3-2.35c3.19,2.48,6.24,5.15,9.34,7.74l15.76,13.19c8.22,6.87,16.41,13.79,24.66,20.63,3.34,2.76,6.92,5.28,11.3,6.14a24.91,24.91,0,0,0,7,.63,26.87,26.87,0,0,0,10.53-2.91c4.53-2.21,8.22-6.31,10.71-10.61l5.28-9.13,21.33-36.87,15.11-26.14.26-.44a15.59,15.59,0,0,0,1.51-11.56,15.29,15.29,0,0,0-6.89-9c-3.49-1.84-7.7-2.77-11.56-1.52a15.6,15.6,0,0,0-9,6.9l-14.53,25.12-22.85,39.52-3.65,6.31a34,34,0,0,1-2.2,3.56l2.34-3a13.54,13.54,0,0,1-2.15,2.13l3-2.34a12.29,12.29,0,0,1-2.19,1.28l3.58-1.51a11.4,11.4,0,0,1-2.67.74l4-.54a11.15,11.15,0,0,1-2.45,0l4,.54a12,12,0,0,1-2.42-.64l3.58,1.51a11.83,11.83,0,0,1-2.57-1.5l3,2.34c-4.82-3.91-9.53-8-14.29-11.94L284.27,451l-7.41-6.19-1.41-1.18a77,77,0,0,0-8.47-6.46,32.86,32.86,0,0,0-4.63-2.23,23.43,23.43,0,0,0-5.23-1.73,38.54,38.54,0,0,0-7.18-.75,58,58,0,0,0-7.68.87c-1.87.29-3.71.7-5.54,1.12l-2.47.57-12.87,3c-8.39,1.94-16.78,3.9-25.17,5.81-.43.1-.86.19-1.3.26l4-.54a11,11,0,0,1-2.5,0l4,.53a10.86,10.86,0,0,1-2.62-.73l3.58,1.51a12.19,12.19,0,0,1-2.09-1.23l3,2.34a12.84,12.84,0,0,1-2.22-2.2l2.34,3a14.12,14.12,0,0,1-1.79-3.11l1.51,3.59a15.31,15.31,0,0,1-1-3.66l.53,4a16.39,16.39,0,0,1,0-4l-.54,4a484.59,484.59,0,0,0,4.94-64.91,15.5,15.5,0,0,0-4.4-10.61,15.23,15.23,0,0,0-10.6-4.39c-3.88.17-7.91,1.45-10.61,4.39a15.68,15.68,0,0,0-4.39,10.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
static/hero.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
static/originalblahaj.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,48 +0,0 @@
summary {
font-weight: normal;
font-size: 25px;
margin: 10px 0;
width: fit-content;
border-bottom: 2px solid var(--fg);
user-select: none;
cursor: pointer;
}
#cover {
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 200px;
}
.listing {
display: block;
background: rgba(0, 0, 0, 0.4);
border-radius: 0.5rem;
padding: 0.25rem;
margin: 1rem 0;
}
h3 {
margin: 0;
margin-bottom: 0.25rem;
}
.listing.positive h3 {
color: #7f7;
}
.listing.negative h3 {
color: #f77;
}
#credits, #share, #github { /* I should use a class, obviously, but I'm editing this with the GitHub web editor so I can only edit one file at a time lollll */
text-align: center;
}
#error {
background: rgba(0, 0, 0, 0.4);
border: 2px solid #f77;
padding: 0.5rem;
border-radius: 0.5rem;
}

25
svelte.config.js Normal file
View file

@ -0,0 +1,25 @@
import preprocess from "svelte-preprocess";
import adapter from "@sveltejs/adapter-static";
const dev = process.argv.includes('dev');
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
pages: 'docs',
assets: 'docs',
fallback: 'index.html'
}),
paths: {
base: dev ? '' : '/blahaj',
},
},
preprocess: [
preprocess({
postcss: true,
}),
],
};
export default config;

BIN
thumb.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View file

@ -1 +0,0 @@
/*! https://mths.be/utf8js v3.0.0 by @mathias */(function(v){var a=String.fromCharCode;function h(r){for(var e=[],n=0,x=r.length,t,u;n<x;)t=r.charCodeAt(n++),t>=55296&&t<=56319&&n<x?(u=r.charCodeAt(n++),(u&64512)==56320?e.push(((t&1023)<<10)+(u&1023)+65536):(e.push(t),n--)):e.push(t);return e}function c(r){for(var e=r.length,n=-1,x,t="";++n<e;)x=r[n],x>65535&&(x-=65536,t+=a(x>>>10&1023|55296),x=56320|x&1023),t+=a(x);return t}function s(r){if(r>=55296&&r<=57343)throw Error("Lone surrogate U+"+r.toString(16).toUpperCase()+" is not a scalar value")}function l(r,e){return a(r>>e&63|128)}function d(r){if((r&4294967168)==0)return a(r);var e="";return(r&4294965248)==0?e=a(r>>6&31|192):(r&4294901760)==0?(s(r),e=a(r>>12&15|224),e+=l(r,6)):(r&4292870144)==0&&(e=a(r>>18&7|240),e+=l(r,12),e+=l(r,6)),e+=a(r&63|128),e}function y(r){for(var e=h(r),n=e.length,x=-1,t,u="";++x<n;)t=e[x],u+=d(t);return u}function i(){if(F>=f)throw Error("Invalid byte index");var r=o[F]&255;if(F++,(r&192)==128)return r&63;throw Error("Invalid continuation byte")}function C(){var r,e,n,x,t;if(F>f)throw Error("Invalid byte index");if(F==f)return!1;if(r=o[F]&255,F++,(r&128)==0)return r;if((r&224)==192){if(e=i(),t=(r&31)<<6|e,t>=128)return t;throw Error("Invalid continuation byte")}if((r&240)==224){if(e=i(),n=i(),t=(r&15)<<12|e<<6|n,t>=2048)return s(t),t;throw Error("Invalid continuation byte")}if((r&248)==240&&(e=i(),n=i(),x=i(),t=(r&7)<<18|e<<12|n<<6|x,t>=65536&&t<=1114111))return t;throw Error("Invalid UTF-8 detected")}var o,f,F;function b(r){o=h(r),f=o.length,F=0;for(var e=[],n;(n=C())!==!1;)e.push(n);return c(e)}v.version="3.0.0",v.encode=y,v.decode=b})(typeof exports=="undefined"?this.utf8={}:exports);

7
vite.config.js Normal file
View file

@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
const config = {
plugins: [sveltekit()]
};
export default config;