├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── bug_report.md │ └── feature-request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENCE ├── README.md ├── common ├── GameEvent.ts ├── Item.ts ├── Region.ts ├── Server.ts ├── WanderingMerchant.ts ├── api │ ├── APIEventType.ts │ ├── APIGameEvent.ts │ └── index.js └── index.js ├── components ├── GameEventTableCell.tsx ├── MerchantTableCell.tsx ├── NavBar.tsx ├── SideBar.tsx ├── index.js └── modals │ ├── AlarmConfigModal.tsx │ ├── ChangeLogModal.tsx │ ├── GitHubModal.tsx │ └── MerchantConfigModal.tsx ├── data ├── data.json ├── events.json ├── itemMapping.json ├── itemRarity.json ├── merchantSchedules.json ├── merchants.json ├── msgs.json └── regions.json ├── next-env.d.ts ├── next-i18next.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── alarms.tsx ├── api │ ├── hello.ts │ ├── items.ts │ ├── regions │ │ ├── [region].ts │ │ └── all.ts │ └── server-maintenance.ts ├── index.tsx └── merchants.tsx ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── images │ ├── LA_Mokko_Seed.png │ ├── language_icon.png │ ├── merchantLocations │ │ ├── ARICER_ROHENDEL │ │ │ ├── ARICER_ROHENDEL_BREEZESOME_BRAE.png │ │ │ ├── ARICER_ROHENDEL_ELZOWINS_SHADE.png │ │ │ ├── ARICER_ROHENDEL_GLASS_LOTUS_LAKE.png │ │ │ ├── ARICER_ROHENDEL_LAKE_SHIVERWAVE.png │ │ │ └── ARICER_ROHENDEL_XENEELA_RUINS.png │ │ ├── BEN_RETHRAMIS │ │ │ ├── BEN_RETHRAMIS_ANKUMO_MOUNTAIN.png │ │ │ ├── BEN_RETHRAMIS_LOGHILL.png │ │ │ └── BEN_RETHRAMIS_RETHRAMIS_BORDER.png │ │ ├── BURT_EAST_LUTERRA │ │ │ ├── BURT_EAST_LUTERRA_BLACKROSE_CHAPEL.png │ │ │ ├── BURT_EAST_LUTERRA_BOREAS_DOMAIN.png │ │ │ ├── BURT_EAST_LUTERRA_CROCONYS_SEASHORE.png │ │ │ ├── BURT_EAST_LUTERRA_CROCONYS_SEASHORE_NORTH.png │ │ │ ├── BURT_EAST_LUTERRA_CROCONYS_SEASHORE_SOUTH.png │ │ │ └── BURT_EAST_LUTERRA_LEYAR_TERRACE.png │ │ ├── DORELLA_FEITON │ │ │ └── DORELLA_FEITON_KALAJA.png │ │ ├── JEFFREY_SHUSHIRE │ │ │ ├── JEFFREY_SHUSHIRE_BITTERWIND_HILL.png │ │ │ ├── JEFFREY_SHUSHIRE_FROZEN_SEA.png │ │ │ ├── JEFFREY_SHUSHIRE_ICEBLOOD_PLATEAU.png │ │ │ ├── JEFFREY_SHUSHIRE_ICEWING_HEIGHTS.png │ │ │ └── JEFFREY_SHUSHIRE_LAKE_ETERNITY.png │ │ ├── LAITIR_YORN │ │ │ ├── LAITIR_YORN_BLACK_ANVIL_MINE.png │ │ │ ├── LAITIR_YORN_HALL_OF_PROMISE.png │ │ │ ├── LAITIR_YORN_IRON_HAMMER_MINE.png │ │ │ ├── LAITIR_YORN_UNFINISHED_GARDEN.png │ │ │ └── LAITIR_YORN_YORNS_CRADLE.png │ │ ├── LUCAS_YUDIA │ │ │ ├── LUCAS_YUDIA_OZHORN_HILL.png │ │ │ └── LUCAS_YUDIA_SALAND_HILL.png │ │ ├── MAC_ANIKKA │ │ │ ├── MAC_ANIKKA_DELPHI_TOWNSHIP.png │ │ │ ├── MAC_ANIKKA_MELODY_FOREST.png │ │ │ ├── MAC_ANIKKA_PRISMA_VALLEY.png │ │ │ ├── MAC_ANIKKA_RATTAN_HILL.png │ │ │ └── MAC_ANIKKA_TWILIGHT_MISTS.png │ │ ├── MALONE_WEST_LUTERRA │ │ │ ├── MALONE_WEST_LUTERRA_BATTLEBOUND_PLAINS.png │ │ │ ├── MALONE_WEST_LUTERRA_BILBRIN_FOREST.png │ │ │ ├── MALONE_WEST_LUTERRA_LAKEBAR.png │ │ │ ├── MALONE_WEST_LUTERRA_MEDRICK_MONASTERY.png │ │ │ └── MALONE_WEST_LUTERRA_MOUNT_ZAGORAS.png │ │ ├── MORRIS_EAST_LUTERRA │ │ │ ├── MORRIS_EAST_LUTERRA_DYORIKA_PLAIN.png │ │ │ ├── MORRIS_EAST_LUTERRA_FLOWERING_ORCHARD.png │ │ │ └── MORRIS_EAST_LUTERRA_SUNBRIGHT_HILL.png │ │ ├── NOX_ARTHETINE │ │ │ ├── NOX_ARTHETINE_ARID_PATH.png │ │ │ ├── NOX_ARTHETINE_NEBELHORN.png │ │ │ ├── NOX_ARTHETINE_RIZA_FALLS.png │ │ │ ├── NOX_ARTHETINE_SCRAPLANDS.png │ │ │ ├── NOX_ARTHETINE_TOTRICH.png │ │ │ └── NOX_ARTHETINE_WINDBRINGER_HILLS.png │ │ ├── OLIVER_TORTOYK │ │ │ ├── OLIVER_TORTOYK_FOREST_OF_GIANTS.png │ │ │ ├── OLIVER_TORTOYK_SEASWEPT_WOODS.png │ │ │ ├── OLIVER_TORTOYK_SKYREACH_STEPPE.png │ │ │ └── OLIVER_TORTOYK_SWEETWATER_FOREST.png │ │ ├── PETER_NORTH_VERN │ │ │ ├── PETER_NORTH_VERN_BALANKAR_MOUNTAINS.png │ │ │ ├── PETER_NORTH_VERN_FESNAR_HIGHLAND.png │ │ │ ├── PETER_NORTH_VERN_PARNA_FOREST.png │ │ │ ├── PETER_NORTH_VERN_PORT_KRONA.png │ │ │ └── PETER_NORTH_VERN_VERNESE_FOREST.png │ │ └── RAYNI_PUNIKA │ │ │ ├── RAYNI_PUNIKA_SECRET_FOREST.png │ │ │ ├── RAYNI_PUNIKA_STARSAND_BEACH.png │ │ │ ├── RAYNI_PUNIKA_TIDESHELF_PATH.png │ │ │ └── RAYNI_PUNIKA_TIKATIKA_COLONY.png │ └── saint-bot.png ├── locales │ ├── en │ │ ├── alarmConfig.json │ │ ├── alarms.json │ │ ├── common.json │ │ ├── events.json │ │ ├── merchantConfig.json │ │ └── merchants.json │ └── zh │ │ ├── alarmConfig.json │ │ ├── alarms.json │ │ ├── common.json │ │ ├── events.json │ │ ├── merchantConfig.json │ │ └── merchants.json ├── vercel.svg └── vercel_favicon.ico ├── should_deploy.sh ├── sound.d.ts ├── sounds ├── alert_1.mp3 ├── alert_2.mp3 ├── alert_3.mp3 ├── alert_4.mp3 ├── alert_5.mp3 ├── alert_6.mp3 └── index.js ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── util ├── createTableData.tsx ├── static.ts ├── types └── types.ts └── usePrevious.tsx /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve. 4 | title: "[Bug]:" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. If applicable, local time, local timezone, server time, and server timezone. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]:" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. If applicable, local time, local timezone, server time, and server timezone. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: I have a specific suggestion for Lost Ark Timer! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Pull request checklist 4 | 5 | Please check if your PR fulfills the following requirements: 6 | 7 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) 8 | - [ ] Build (`npm run build`) was run locally and any changes were pushed 9 | - [ ] Lint (`npm run lint`) has passed locally and any fixes were made for failures 10 | 11 | ## Pull request type 12 | 13 | 14 | 15 | 16 | 17 | Please check the type of change your PR introduces: 18 | 19 | - [ ] Bugfix 20 | - [ ] Feature 21 | - [ ] Code style update (formatting, renaming) 22 | - [ ] Refactoring (no functional changes, no api changes) 23 | - [ ] Build related changes 24 | - [ ] Documentation content changes 25 | - [ ] Other (please describe): 26 | 27 | ## What is the current behavior? 28 | 29 | 30 | 31 | 32 | 33 | Issue URL: 34 | 35 | ## What is the new behavior? 36 | 37 | 38 | 39 | - 40 | - 41 | - 42 | 43 | ## Does this introduce a breaking change? 44 | 45 | - [ ] Yes 46 | - [ ] No 47 | 48 | 49 | 50 | ## Other information 51 | 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Lost Ark Timer
3 | Timers for Lost Ark bosses, islands, events, wandering merchants and more! 4 |
Never miss an event again.

5 |

6 | 7 | # LostArkTimer.app Website 8 | 9 | - [Website](https://www.lostarktimer.app/) 10 | 11 | ## Features 12 | 13 | - Event Timers (Islands, Bosses, Special Events) 14 | - Wandering Merchants (powered by [Saintbot](http://saint-bot.webflow.io)) 15 | - 6 Alert Sounds 16 | - Ability to disable alarms (once, 12 hours, until reset, or "permanently" [currently 3 weeks]) 17 | - Ability to hide disabled alarms 18 | - Show less repeating events like the Grand Prix 19 | 20 | Planned: 21 | 22 | - Daily Reset countdown 23 | - Procyon compass checkboxes 24 | - Wandering Merchant Ships 25 | - Dark / Light mode toggle 26 | 27 | ## Contribution 28 | 29 | Contributors are highly welcome! 30 | 31 | Join the [Discord](https://discord.gg/beFb23WgNC) to provide feedback, suggest features, and report bugs. 32 | 33 | ### Development 34 | 35 | ```sh 36 | $ npm install 37 | $ npm run dev 38 | ``` 39 | 40 | Note: 41 | The merchants feature will not work in development until I figure out how to mock a websocket server with running data for development. 42 | 43 | ### Libraries 44 | 45 | This project is styled with [tailwindcss](https://tailwindcss.com) and [daisyUI](https://daisyui.com). 46 | 47 | ## Deployment 48 | 49 | This application is hosted on Vercel. 50 | 51 | ## License 52 | 53 | GNU GPLv3 54 | -------------------------------------------------------------------------------- /common/GameEvent.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Interval } from 'luxon' 2 | import { APIEventType, APIGameEvent } from './api' 3 | import { v4 as uuidv4 } from 'uuid' 4 | class GameEvent { 5 | eventType: APIEventType 6 | gameEvent: APIGameEvent 7 | uuid: string 8 | times: Array = [] 9 | disabled: DateTime | null = null 10 | groupName: string | null = null 11 | constructor(et: APIEventType, ge: APIGameEvent) { 12 | this.eventType = et 13 | this.gameEvent = ge 14 | this.uuid = uuidv4() 15 | } 16 | addTime(t: Interval) { 17 | this.times.push(t) 18 | } 19 | latest(t: DateTime): Interval { 20 | return this.times.filter((ti) => ti.start.diff(t).valueOf() > 0)[0] 21 | } 22 | } 23 | 24 | export default GameEvent 25 | -------------------------------------------------------------------------------- /common/Item.ts: -------------------------------------------------------------------------------- 1 | class Item { 2 | name: String 3 | 4 | constructor(name: String) { 5 | this.name = name 6 | } 7 | } 8 | 9 | export default Item 10 | -------------------------------------------------------------------------------- /common/Region.ts: -------------------------------------------------------------------------------- 1 | import Server from './Server' 2 | 3 | class Region { 4 | name: String 5 | servers: Server[] 6 | 7 | constructor(name: String, servers: Server[]) { 8 | this.name = name 9 | this.servers = servers 10 | } 11 | } 12 | 13 | export default Region 14 | -------------------------------------------------------------------------------- /common/Server.ts: -------------------------------------------------------------------------------- 1 | import WanderingMerchant from './WanderingMerchant' 2 | 3 | class Server { 4 | name: String 5 | merchants: WanderingMerchant[] 6 | constructor(name: String, merchants: WanderingMerchant[]) { 7 | this.name = name 8 | this.merchants = merchants 9 | } 10 | } 11 | 12 | export default Server 13 | -------------------------------------------------------------------------------- /common/WanderingMerchant.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Interval } from 'luxon' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import Item from './Item' 4 | 5 | type MerchantItems = { rapport: number[]; cards: number[]; cooking: number[] } 6 | class WanderingMerchant { 7 | name: string 8 | continent: string 9 | items: MerchantItems 10 | spawned: boolean = false 11 | scheduleId: 1 | 2 | 3 12 | schedule: Interval[] 13 | locationImages: { [key: string]: string } 14 | 15 | location: string | null = null 16 | goodItems: string[] | null = null 17 | spawnTime: number | null = null 18 | 19 | uuid: string 20 | 21 | constructor( 22 | name: string, 23 | items: MerchantItems, 24 | continent: string, 25 | scheduleId: number, 26 | locationImages: { [key: string]: string }, 27 | schedule: Interval[] 28 | ) { 29 | this.name = name 30 | this.items = items 31 | this.continent = continent 32 | this.scheduleId = scheduleId as 1 | 2 | 3 33 | this.locationImages = locationImages 34 | this.schedule = schedule 35 | this.uuid = uuidv4() 36 | // this.expectedSpawnTime = expectedSpawnTime 37 | } 38 | 39 | setSpawn( 40 | // continent: String | null, 41 | location: string | null, 42 | goodItem: string | null, 43 | spawnTime: number | null 44 | ) { 45 | // this.continent = continent 46 | this.location = location 47 | this.goodItems = goodItem?.split('][') ?? null 48 | this.spawnTime = spawnTime 49 | this.spawned = true 50 | } 51 | inProgress(serverTime: DateTime) { 52 | if (this.spawnTime != null) return true 53 | for (const interval of this.schedule) { 54 | if ( 55 | interval.start.diff(serverTime).toMillis() < 0 && 56 | interval.end.diff(serverTime).toMillis() > 0 57 | ) 58 | return interval 59 | } 60 | return null 61 | } 62 | nextSpawnTime(serverTime: DateTime) { 63 | for (let idx = 0; idx < this.schedule.length; idx++) { 64 | let interval = this.schedule[idx] 65 | let startDiff = interval.start.diff(serverTime).toMillis() 66 | let endDiff = interval.end.diff(serverTime).toMillis() 67 | 68 | // case: time before first event of day 69 | if (startDiff > 0) return interval 70 | // case: in progress 71 | if (startDiff < 0 && endDiff > 0) 72 | return idx + 1 < this.schedule.length 73 | ? this.schedule[idx + 1] 74 | : this.schedule[0] 75 | } 76 | // case: time is after last event of day 77 | return this.schedule[0] 78 | } 79 | unsetSpawn() { 80 | this.spawned = false 81 | this.location = null 82 | this.goodItems = null 83 | this.spawnTime = null 84 | } 85 | } 86 | 87 | export default WanderingMerchant 88 | -------------------------------------------------------------------------------- /common/api/APIEventType.ts: -------------------------------------------------------------------------------- 1 | class APIEventType { 2 | id: number 3 | name: string 4 | iconUrl: string 5 | constructor(id: number, name: string, iconUrl: string) { 6 | this.id = id 7 | this.name = name 8 | this.iconUrl = iconUrl 9 | } 10 | } 11 | 12 | export default APIEventType 13 | -------------------------------------------------------------------------------- /common/api/APIGameEvent.ts: -------------------------------------------------------------------------------- 1 | class APIGameEvent { 2 | id: string 3 | name: string 4 | iconUrl: string 5 | minItemLevel: number 6 | constructor(id: string, name: string, iconUrl: string, minItemLevel: number) { 7 | this.id = id 8 | this.name = name 9 | this.iconUrl = iconUrl 10 | this.minItemLevel = minItemLevel 11 | } 12 | } 13 | 14 | export default APIGameEvent 15 | -------------------------------------------------------------------------------- /common/api/index.js: -------------------------------------------------------------------------------- 1 | import APIEventType from './APIEventType' 2 | import APIGameEvent from './APIGameEvent' 3 | 4 | export { APIEventType, APIGameEvent } 5 | -------------------------------------------------------------------------------- /common/index.js: -------------------------------------------------------------------------------- 1 | import GameEvent from './GameEvent' 2 | export { GameEvent } 3 | -------------------------------------------------------------------------------- /components/GameEventTableCell.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { GameEvent } from '../common' 3 | 4 | import Image from 'next/image' 5 | import { DateTime, Duration, Zone } from 'luxon' 6 | import classNames from 'classnames' 7 | import useLocalStorage from '@olerichter00/use-localstorage' 8 | import { generateTimestampStrings } from '../util/createTableData' 9 | import { useTranslation } from 'next-i18next' 10 | type CellProps = { 11 | gameEvent: GameEvent 12 | serverTime: DateTime 13 | localizedTZ: Zone 14 | view24HrTime: boolean | undefined 15 | } 16 | 17 | const GameEventTableCell = (props: CellProps): React.ReactElement => { 18 | const { t } = useTranslation('events') 19 | const { gameEvent, serverTime, localizedTZ, view24HrTime } = props 20 | const [disabledAlarms, setDisabledAlarms] = useLocalStorage<{ 21 | [key: string]: number 22 | }>('disabledAlarms', {}) 23 | const [hideGrandPrix, setHideGrandPrix] = useLocalStorage( 24 | 'hideGrandPrix', 25 | false 26 | ) 27 | const [timeUntil, setTimeUntil] = useState( 28 | Duration.fromMillis( 29 | gameEvent.latest(serverTime)?.start.diff(serverTime).toMillis() 30 | ) 31 | ) 32 | useEffect(() => { 33 | if (!gameEvent.disabled) { 34 | const timer = setInterval(() => { 35 | // setServerTime() 36 | setTimeUntil( 37 | Duration.fromMillis( 38 | gameEvent 39 | .latest(serverTime) 40 | ?.start.diff(DateTime.now().setZone(serverTime.zone)) 41 | .toMillis() 42 | ) 43 | ) 44 | }, 1000) 45 | 46 | return () => { 47 | clearInterval(timer) // Return a funtion to clear the timer so that it will stop being called on unmount 48 | } 49 | } 50 | }, []) 51 | 52 | if (gameEvent === null) { 53 | return ( 54 | 55 | ) 56 | } 57 | return ( 58 | 63 |
68 |
69 | 74 |
75 |
76 |
77 | 78 | {!(hideGrandPrix && gameEvent.groupName) && 79 | `[${gameEvent.gameEvent.minItemLevel}] `} 80 | {(hideGrandPrix && gameEvent.groupName) || 81 | t(`${gameEvent.gameEvent.id}`)} 82 | 83 | {gameEvent.disabled 84 | ? null 85 | : `-${timeUntil.toFormat('hh:mm:ss')}`} 86 | 87 | 88 | 89 | {gameEvent.times.map((t, idx) => 90 | generateTimestampStrings( 91 | gameEvent, 92 | t, 93 | serverTime, 94 | localizedTZ, 95 | view24HrTime || false, 96 | idx 97 | ) 98 | )} 99 | 100 |
101 |
102 |
103 | 383 | 384 | ) 385 | } 386 | /**return ( 387 | <> 388 | 390 | 391 | 392 | 393 | 394 | className={`${ 395 | t.start.diff(serverTime).valueOf() > 0 396 | ? 'text-success' 397 | : 'text-slate-500' 398 | }`} 399 | >{`${t.start.toLocaleString(DateTime.TIME_24_SIMPLE)}${ 400 | !t.isEmpty() 401 | ? ` - ${t.end.toLocaleString(DateTime.TIME_24_SIMPLE)}` 402 | : '' 403 | } `}{' '} 404 | {idx < gameEvent.times.length - 1 ? ' / ' : ''} 405 | */ 406 | export default GameEventTableCell 407 | -------------------------------------------------------------------------------- /components/MerchantTableCell.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import merchantSchedules from '../data/merchantSchedules.json' 3 | import itemMapping from '../data/itemMapping.json' 4 | import itemRarity from '../data/itemRarity.json' 5 | import { DateTime, Interval, Zone } from 'luxon' 6 | import Image from 'next/image' 7 | import classNames from 'classnames' 8 | import { generateTimestampStrings } from '../util/createTableData' 9 | import WanderingMerchant from '../common/WanderingMerchant' 10 | import useLocalStorage from '@olerichter00/use-localstorage' 11 | import { useTranslation } from 'next-i18next' 12 | 13 | type ItemMappingKey = keyof typeof itemMapping 14 | 15 | interface CellProps { 16 | merchant: WanderingMerchant 17 | serverTime: DateTime 18 | localizedTZ: Zone 19 | view24HrTime: boolean 20 | } 21 | let mSchedules: { [key: number]: Interval[] } = {} 22 | 23 | const MerchantTableCell = (props: CellProps): React.ReactElement => { 24 | const { t } = useTranslation('merchants') 25 | const { merchant, serverTime, localizedTZ, view24HrTime } = props 26 | Object.entries(merchantSchedules).forEach( 27 | ([key, schedule]) => 28 | (mSchedules[Number(key)] = schedule.map(({ h, m }) => { 29 | let start = DateTime.fromObject( 30 | { hour: h, minute: m } 31 | // { zone: serverZone } 32 | ) 33 | return Interval.fromDateTimes(start, start.plus({ minutes: 25 })) 34 | })) 35 | ) 36 | const [nextSpawnCountdown, setNextSpawnCountdown] = useState( 37 | merchant 38 | .nextSpawnTime(serverTime) 39 | .start.setZone(serverTime.zone) 40 | .diff(DateTime.now()) 41 | ) 42 | useEffect(() => { 43 | const timer = setInterval(() => { 44 | setNextSpawnCountdown( 45 | merchant 46 | .nextSpawnTime(serverTime) 47 | .start.setZone(serverTime.zone) 48 | .diff(DateTime.now()) 49 | ) 50 | }, 1000) 51 | return () => { 52 | clearInterval(timer) 53 | } 54 | }) 55 | const [hideMerchantItems, setHideMerchantItems] = useLocalStorage( 56 | 'hideMerchantItems', 57 | false 58 | ) 59 | const [hidePotentialSpawns, sethidePotentialSpawns] = useLocalStorage( 60 | 'hidePotentialMerchantLocationSpawns', 61 | false 62 | ) 63 | let imageUrl = '' 64 | if (merchant.location) { 65 | imageUrl = 66 | merchant.locationImages[ 67 | Object.keys(merchant.locationImages).find( 68 | (k) => k.toLowerCase() === merchant.location?.toLowerCase() 69 | ) || '' 70 | ] 71 | } 72 | const gradientColor = (id: 0 | 1 | 2 | 3) => { 73 | if (id === 0) return 'bg-gradient-to-br from-[#1e2f08] to-[#4c8204]' 74 | if (id === 1) return 'bg-gradient-to-br from-[#082c3b] to-[#0479a9]' 75 | if (id === 2) return 'bg-gradient-to-br from-[#2e083b] to-[#8004a9]' 76 | if (id === 3) return 'bg-gradient-to-br from-[#392509] to-[#a16305]' 77 | } 78 | const textColor = (id: 0 | 1 | 2 | 3) => { 79 | if (id === 0) return 'text-[#6fc300]' 80 | if (id === 1) return 'text-[#00b5ff]' 81 | if (id === 2) return 'text-[#bf00fe]' 82 | if (id === 3) return 'text-[#f39303]' 83 | } 84 | const rarity = (id: number): 0 | 1 | 2 | 3 => { 85 | let ir = itemRarity as { [key: string]: 0 | 1 | 2 | 3 } 86 | return ir[String(id)] 87 | } 88 | const merchantGoodItemToRarity = (goodItem: string | null): string => { 89 | if (goodItem) { 90 | let first = goodItem.split(' ')[0] 91 | switch (first) { 92 | case 'No': 93 | return 'text-[#6fc300]' 94 | case 'Seria': 95 | case 'Sian': 96 | return 'text-[#00b5ff]' 97 | case 'Madnick': 98 | case 'Mokamoka': 99 | case 'Kaysarr': 100 | return 'text-[#bf00fe]' 101 | case 'Wei': 102 | case 'Legendary': 103 | return 'text-[#f39303]' 104 | default: 105 | return '' 106 | } 107 | } 108 | return '' 109 | } 110 | const onClickOpenWindow = (imageUrl: string, title: string) => { 111 | window.open( 112 | `https://i.imgur.com/${imageUrl}`, 113 | title, 114 | 'left=20,top=20,width=1000,height=600,toolbar=0,resizable=1,noopener=1,noreferrer=1' 115 | ) 116 | return false 117 | } 118 | const iconURL = (item: number) => { 119 | let iconName = itemMapping[String(item) as ItemMappingKey].fileName 120 | return `https://lostarkcodex.com/icons/${iconName}` 121 | } 122 | //class="dropdown m-2 flex basis-1/2 bg-stone-100 p-2 hover:cursor-pointer dark:bg-base-100 dark:hover:bg-base-100/70" 123 | return ( 124 | 137 | {/*
*/} 138 |
139 |
140 | 141 | 148 | {merchant.name} ({t(`locations.${merchant.continent}`)}){' '} 149 | {merchant.inProgress(serverTime) ? ( 150 | 151 | {' '} 152 | {serverTime 153 | .set({ 154 | minute: 30, 155 | }) 156 | .setZone(localizedTZ) 157 | .toLocaleString( 158 | view24HrTime 159 | ? DateTime.TIME_24_SIMPLE 160 | : DateTime.TIME_SIMPLE 161 | )}{' '} 162 | -{' '} 163 | {serverTime 164 | .set({ minute: 55 }) 165 | .setZone(localizedTZ) 166 | .toLocaleString( 167 | view24HrTime 168 | ? DateTime.TIME_24_SIMPLE 169 | : DateTime.TIME_SIMPLE 170 | )} 171 | 172 | ) : null} 173 | 174 | 175 | {merchant.inProgress(serverTime) ? ( 176 |
177 | 178 | {t('location')}:{' '} 179 | 184 | {merchant.spawned ? ( 185 | 188 | onClickOpenWindow(imageUrl, merchant.location || '') 189 | } 190 | > 191 | {t(`locations.${merchant.location}`)} 192 | 193 | ) : ( 194 | 'Unknown' 195 | )} 196 | 197 | 198 |
199 | Item:{' '} 200 | {merchant.goodItems 201 | ? merchant.goodItems.map((goodItem, index) => ( 202 | 208 | {goodItem} 209 | {index === 0 ? , : ''} 210 | 211 | )) 212 | : 'Unknown'} 213 |
214 | ) : null} 215 |
216 | 222 | {t('next-spawn')}:{' '} 223 | {merchant 224 | .nextSpawnTime(serverTime) 225 | ?.start.setZone(localizedTZ) 226 | .toLocaleString( 227 | view24HrTime ? DateTime.TIME_24_SIMPLE : DateTime.TIME_SIMPLE 228 | )} 229 | 237 | -{nextSpawnCountdown.toFormat('hh:mm:ss')} 238 | 239 | 240 | {merchant.spawned}{' '} 241 |
242 |
247 | {t('potential-spawns')} 248 | {Object.entries(merchant.locationImages).map( 249 | ([locationName, imgUrl], idx, arr) => ( 250 | 251 | onClickOpenWindow(imgUrl, locationName)} 254 | > 255 | {t(`locations.${locationName}`)} 256 | 257 | {idx + 1 < arr.length ?
: ''} 258 |
259 | ) 260 | )} 261 |
262 |
263 |
264 |
270 | {[ 271 | { title: 'Rapport', items: merchant.items.rapport }, 272 | { title: 'Cards', items: merchant.items.cards }, 273 | { title: 'Cooking', items: merchant.items.cooking }, 274 | ].map(({ title, items }, idx) => { 275 | return items.length ? ( 276 |
283 |
284 | {title} 285 | {items.map((item) => ( 286 |
293 | 300 | 301 | {t(`items.${item}.name`)} 302 | 303 |
304 | ))} 305 |
306 |
307 | ) : null 308 | })} 309 |
310 | {/*
*/} 311 | 312 | ) 313 | } 314 | 315 | export default MerchantTableCell 316 | -------------------------------------------------------------------------------- /components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import Link from 'next/link' 3 | import { useRouter } from 'next/router' 4 | import { useTranslation } from 'next-i18next' 5 | import { IconLanguage } from '@tabler/icons' 6 | const NavBar = () => { 7 | const { t } = useTranslation('common') 8 | 9 | const router = useRouter() 10 | return ( 11 | <> 12 |
13 |
17 | 18 | 30 | 31 |
32 |
33 |
34 |
35 |
36 | 41 | 42 | {t('alarm-link-text')} 43 | 44 | 45 | 46 | 51 | 52 | {t('merchant-link-text')} 53 | 54 | 55 |
56 |
57 |
58 | Lost Ark Timer 59 | 60 |
61 | 67 | Discord 68 | 69 | 70 | 76 | GitHub 77 | 78 | 79 | 85 | Support 86 | 87 |
88 |
89 |
90 | 91 | 104 |
105 |
106 | 107 | ) 108 | } 109 | 110 | export default NavBar 111 | -------------------------------------------------------------------------------- /components/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | IconBrandGithub, 4 | IconBrandPaypal, 5 | IconCoffee, 6 | IconFileCode, 7 | } from '@tabler/icons' 8 | 9 | const SideBar = () => { 10 | return ( 11 | 36 | ) 37 | } 38 | 39 | export default SideBar 40 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | import AlarmConfigModal from './modals/AlarmConfigModal' 2 | import ChangeLogModal from './modals/ChangeLogModal' 3 | import GameEventTableCell from './GameEventTableCell' 4 | import GitHubModal from './modals/GitHubModal' 5 | import MerchantConfigModal from './modals/MerchantConfigModal' 6 | import SideBar from './SideBar' 7 | 8 | export { 9 | AlarmConfigModal, 10 | MerchantConfigModal, 11 | ChangeLogModal, 12 | GameEventTableCell, 13 | GitHubModal, 14 | SideBar, 15 | } 16 | -------------------------------------------------------------------------------- /components/modals/AlarmConfigModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Howl, Howler } from 'howler' 3 | import { alert1, alert2, alert3, alert4, alert5, alert6 } from '../../sounds' 4 | import useLocalStorage from '@olerichter00/use-localstorage' 5 | import { IconVolume2, IconVolume3 } from '@tabler/icons' 6 | import { useTranslation } from 'next-i18next' 7 | 8 | const sounds = { 9 | 'Alert 1': alert1, 10 | 'Alert 2': alert2, 11 | 'Alert 3': alert3, 12 | 'Alert 4': alert4, 13 | 'Alert 5': alert5, 14 | 'Alert 6': alert6, 15 | } 16 | type AlertSoundKeys = 17 | | 'Alert 1' 18 | | 'Alert 2' 19 | | 'Alert 3' 20 | | 'Alert 4' 21 | | 'Alert 5' 22 | | 'Alert 6' 23 | const AlarmConfigModal = () => { 24 | const { t } = useTranslation('alarmConfig') 25 | 26 | const [viewLocalizedTime, setViewLocalizedTime] = useLocalStorage( 27 | 'viewLocalizedTime', 28 | true 29 | ) 30 | const [desktopNotifications, setDesktopNotifications] = 31 | useLocalStorage('desktopNotifications', false) 32 | const [view24HrTime, setView24HrTime] = useLocalStorage( 33 | 'view24HrTime', 34 | false 35 | ) 36 | const defaultTheme = () => { 37 | return (localStorage.getItem('darkMode') || window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) 38 | } 39 | const [darkMode, setDarkMode] = useLocalStorage('darkMode', defaultTheme) 40 | const [alertSound, setAlertSound] = useLocalStorage( 41 | 'alertSound', 42 | 'muted' 43 | ) 44 | const [hideGrandPrix, setHideGrandPrix] = useLocalStorage( 45 | 'hideGrandPrix', 46 | false 47 | ) 48 | const [moveDisabledEventsBottom, setMoveDisabledEventsBottom] = 49 | useLocalStorage('moveDisabledEventsBottom', false) 50 | const [hideDisabledEvents, setHideDisableEvents] = useLocalStorage( 51 | 'hideDisabledEvents', 52 | false 53 | ) 54 | const [disabledAlarms, setDisabledAlarms] = useLocalStorage<{ 55 | [key: string]: number 56 | }>('disabledAlarms', {}) 57 | const [volume, setVolume] = useLocalStorage('volume', 0.4) 58 | return ( 59 | <> 60 | 61 |
62 |
63 |
64 |

65 | {t('alarm-settings')} 66 |

67 |
68 |
69 |
70 | 85 | 98 | 114 |
115 | 121 |
122 |
123 |
124 | 155 | 168 | 181 | 194 | 223 |
224 | 225 | { 233 | setVolume(event.target.valueAsNumber) 234 | }} 235 | disabled={alertSound === 'muted'} 236 | onMouseUpCapture={(event) => { 237 | let s = new Howl({ 238 | src: sounds[ 239 | alertSound as AlertSoundKeys 240 | ] as unknown as string, 241 | }) 242 | Howler.stop() 243 | s.play() 244 | }} 245 | /> 246 | 247 |
248 |
249 |
250 |
251 | 254 |
255 |
256 |
257 | 258 | ) 259 | } 260 | 261 | export default AlarmConfigModal 262 | -------------------------------------------------------------------------------- /components/modals/ChangeLogModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ChangeLogModal = () => { 4 | return ( 5 | <> 6 | 7 | 76 | 77 | ) 78 | } 79 | 80 | export default ChangeLogModal 81 | -------------------------------------------------------------------------------- /components/modals/GitHubModal.tsx: -------------------------------------------------------------------------------- 1 | import { IconBrandDiscord } from '@tabler/icons' 2 | import React from 'react' 3 | 4 | const GitHubModal = () => { 5 | return ( 6 | <> 7 | 8 | 35 | 36 | ) 37 | } 38 | 39 | export default GitHubModal 40 | -------------------------------------------------------------------------------- /components/modals/MerchantConfigModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useLocalStorage from '@olerichter00/use-localstorage' 3 | import { useTranslation } from 'next-i18next' 4 | 5 | const MerchantConfigModal = () => { 6 | const { t } = useTranslation('merchantConfig') 7 | const [viewLocalizedTime, setViewLocalizedTime] = useLocalStorage( 8 | 'viewLocalizedTime', 9 | true 10 | ) 11 | const [view24HrTime, setView24HrTime] = useLocalStorage( 12 | 'view24HrTime', 13 | false 14 | ) 15 | const defaultTheme = () => { 16 | return (localStorage.getItem('darkMode') || window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) 17 | } 18 | const [darkMode, setDarkMode] = useLocalStorage('darkMode', defaultTheme) 19 | const [alertSound, setAlertSound] = useLocalStorage( 20 | 'alertSound', 21 | 'muted' 22 | ) 23 | const [hideGrandPrix, setHideGrandPrix] = useLocalStorage( 24 | 'hideGrandPrix', 25 | false 26 | ) 27 | const [moveDisabledEventsBottom, setMoveDisabledEventsBottom] = 28 | useLocalStorage('moveDisabledEventsBottom', false) 29 | const [hideDisabledEvents, setHideDisableEvents] = useLocalStorage( 30 | 'hideDisabledEvents', 31 | false 32 | ) 33 | 34 | const [hideMerchantItems, setHideMerchantItems] = useLocalStorage( 35 | 'hideMerchantItems', 36 | false 37 | ) 38 | const [hidePotentialSpawns, sethidePotentialSpawns] = useLocalStorage( 39 | 'hidePotentialMerchantLocationSpawns', 40 | false 41 | ) 42 | const [disabledAlarms, setDisabledAlarms] = useLocalStorage<{ 43 | [key: string]: number 44 | }>('disabledAlarms', {}) 45 | const [volume, setVolume] = useLocalStorage('volume', 0.4) 46 | return ( 47 | <> 48 | 53 |
54 |
55 |
56 |

57 | {t('merchant-settings')} 58 |

59 |
60 |
61 |
62 | 75 | 90 |
91 |
92 | 105 | 118 | 131 |
132 |
133 |
134 | 140 |
141 |
142 |
143 | 144 | ) 145 | } 146 | 147 | export default MerchantConfigModal 148 | -------------------------------------------------------------------------------- /data/events.json: -------------------------------------------------------------------------------- 1 | {"8006":["Mercenary Join Request","achieve_07_52.webp",250],"4005":["Proving Grounds Co-op Battle","achieve_07_42.webp",250],"4001":["Proving Grounds Competitive Match","achieve_13_5.webp",250],"4002":["Proving Grounds Team Deathmatch","achieve_13_5.webp",250],"4004":["Proving Grounds Team Elimination Match","achieve_13_5.webp",250],"8007":["Siege PvP Base Entry","achieve_14_150.webp",250],"7046":["Asura Island","island_icon_76.webp",250],"7047":["Drumbeat Island","island_icon_95.webp",250],"7038":["Forpe","island_icon_26.webp",250],"7042":["Harmony Island","island_icon_55.webp",250],"7043":["Lagoon Island","island_icon_58.webp",250],"7049":["Lush Reed Island","island_icon_43.webp",250],"7040":["Medeia","island_icon_41.webp",250],"7041":["Monte Island","island_icon_52.webp",250],"7039":["Oblivion Isle","island_icon_35.webp",250],"7044":["Opportunity Isle","island_icon_70.webp",250],"7050":["Phantomwing Island","island_icon_81.webp",250],"7048":["Snowpang Island","island_icon_98.webp",250],"7045":["Tranquil Isle","island_icon_74.webp",250],"7037":["Volare Island","island_icon_8.webp",250],"950":["Arkesia Grand Prix (Great Castle)","island_icon_122.webp",0],"948":["Arkesia Grand Prix (Kalaja)","island_icon_122.webp",0],"947":["Arkesia Grand Prix (Luterra Castle)","island_icon_122.webp",0],"951":["Arkesia Grand Prix (Nia Village)","island_icon_122.webp",0],"945":["Arkesia Grand Prix (Rothun)","island_icon_122.webp",0],"946":["Arkesia Grand Prix (Stern)","island_icon_122.webp",0],"949":["Arkesia Grand Prix (Vern Castle)","island_icon_122.webp",0],"7033":["Alakkir","island_icon_69.webp",250],"7005":["Death's Hold Island","island_icon_65.webp",250],"7016":["Illusion Isle","island_icon_4.webp",250],"7013":["Lullaby Island","island_icon_5.webp",250],"7018":["Spida Island","island_icon_23.webp",250],"7014":["Tooki Island","island_icon_9.webp",250],"8000":["[ Siege Event 1 Score ] Medeia","island_icon_41.webp",250],"8001":["[ Siege Event 1 Score ] Slime Island","island_icon_68.webp",250],"6007":["Gate of Harmony","island_icon_89.webp",302],"6001":["Sailing Co-op: Anikka","achieve_06_55.webp",302],"6000":["Sailing Co-op: Arthetine","achieve_06_55.webp",302],"6002":["Sailing Co-op: Vern","achieve_06_55.webp",302],"1002":["Twisting Chaos Legion","achieve_13_11.webp",302],"1003":["Twisting Darkness Legion","achieve_13_11.webp",302],"1001":["Twisting Phantom Legion","achieve_13_11.webp",302],"1004":["Twisting Plague Legion","achieve_13_11.webp",302],"3008":["Chaotic Chuo","achieve_14_142.webp",380],"3005":["Signatus","achieve_14_142.webp",380],"3003":["Tarsila","achieve_14_142.webp",380],"3011":["Erasmo","island_icon_38.webp",460],"6008":["Gate of Wisdom","island_icon_89.webp",460],"5002":["Nightmare Ghost Ship","island_icon_91.webp",460],"6003":["Sailing Co-op: Rohendel","achieve_06_55.webp",460],"7019":["Shangra","island_icon_83.webp",460],"1009":["Twisting Phantom Legion","achieve_13_11.webp",460],"7020":["Unknown Island","island_icon_86.webp",460],"3013":["Magmadon","achieve_14_142.webp",540],"3001":["Proxima","achieve_14_142.webp",540],"7035":["Gesbroy","island_icon_87.webp",600],"6009":["Gate of Earth","island_icon_89.webp",802],"6004":["Sailing Co-op: Yorn","achieve_06_55.webp",802],"1007":["Twisting Plague Legion","achieve_13_11.webp",802],"3006":["Harvest Lord Incarnate","achieve_14_142.webp",880],"3012":["Kohinorr","achieve_14_142.webp",880],"6010":["Gate of Endurance","island_icon_89.webp",960],"6005":["Sailing Co-op: Feiton","achieve_06_55.webp",960],"5003":["Shadow Ghost Ship","island_icon_91.webp",960],"1006":["Twisting Darkness Legion","achieve_13_11.webp",960],"3014":["Ancheladus","achieve_14_142.webp",1040],"3002":["Sol Grande","achieve_14_142.webp",1040],"6011":["Gate of Guidance","island_icon_89.webp",1302],"6006":["Sailing Co-op: Punika","achieve_06_55.webp",1302],"1008":["Twisting Chaos Legion","achieve_13_11.webp",1302],"5004":["Tempest Ghost Ship","island_icon_91.webp",1370],"3007":["Aurion","achieve_14_142.webp",1385],"3004":["Brealeos","achieve_14_142.webp",1385],"3015":["Moake","achieve_14_142.webp",1415],"5005":["Sinful Ghost Ship","island_icon_91.webp",1415],"1010":["Twisting Demon Legion (Appears randomly)","achieve_13_11.webp",1415],"9002":["[Assail]Liebertane-[Siege]-[Raid]Preigelli","achieve_14_154.webp",1445],"9001":["[Assail]Preigelli-[Siege]-[Raid]Liebertane","achieve_14_153.webp",1445],"3016":["Thunderwings","achieve_14_142.webp",1460],"3022":["Hermut (Liebertane)","achieve_14_142.webp",1490],"3021":["Hermut (Preigelli)","achieve_14_142.webp",1490],"5006":["Phantom Ghost Ship","island_icon_91.webp",1490],"9050":["[Battlefield] Tulubik Battlegrounds","achieve_14_151.webp",1490]} -------------------------------------------------------------------------------- /data/itemMapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "920404": { 3 | "name": "Adrenaline-boosting Fluid", 4 | "fileName": "all_quest_01_31.webp" 5 | }, 6 | "920701": { "name": "Back Alley Rum", "fileName": "use_6_49.webp" }, 7 | "920803": { 8 | "name": "Raw Boar Meat", 9 | "fileName": "all_quest_02_126.webp" 10 | }, 11 | "921004": { "name": "Blood Pudding Chunk", "fileName": "use_2_24.webp" }, 12 | "921405": { "name": "Hairplant", "fileName": "use_2_139.webp" }, 13 | "930605": { "name": "Sapphire Sardine", "fileName": "use_3_167.webp" }, 14 | "930902": { "name": "Pit-A-Pat Macaron", "fileName": "use_5_213.webp" }, 15 | "931505": { "name": "Dry-aged Meat", "fileName": "use_2_193.webp" }, 16 | "931512": { 17 | "name": "Hot Chocolate Coffee", 18 | "fileName": "all_quest_02_32.webp" 19 | }, 20 | "63200008": { "name": "Wei", "fileName": "use_2_13.webp" }, 21 | "63200024": { "name": "Thunder Wings", "fileName": "use_2_13.webp" }, 22 | "63200030": { "name": "Mokamoka", "fileName": "use_2_13.webp" }, 23 | "63200035": { "name": "Krause", "fileName": "use_2_13.webp" }, 24 | "63200038": { "name": "Madnick", "fileName": "use_2_13.webp" }, 25 | "63200039": { "name": "Thar", "fileName": "use_2_13.webp" }, 26 | "63200050": { "name": "Gnosis", "fileName": "use_2_13.webp" }, 27 | "63200053": { "name": "Kaysarr", "fileName": "use_2_13.webp" }, 28 | "63200057": { "name": "Kaldor", "fileName": "use_2_13.webp" }, 29 | "63200094": { "name": "Varut", "fileName": "use_2_13.webp" }, 30 | "63200095": { "name": "Prideholme Neria", "fileName": "use_2_13.webp" }, 31 | "63200102": { "name": "Thunder", "fileName": "use_2_13.webp" }, 32 | "63200104": { "name": "Meehan", "fileName": "use_2_13.webp" }, 33 | "63200106": { "name": "Cassleford", "fileName": "use_2_13.webp" }, 34 | "63200108": { "name": "Alifer", "fileName": "use_2_13.webp" }, 35 | "63200111": { "name": "Seria", "fileName": "use_2_13.webp" }, 36 | "63200118": { "name": "Nox", "fileName": "use_2_13.webp" }, 37 | "63200122": { "name": "Eolh", "fileName": "use_2_13.webp" }, 38 | "63200136": { "name": "Stern Neria", "fileName": "use_2_13.webp" }, 39 | "63200144": { "name": "Sian", "fileName": "use_2_13.webp" }, 40 | "63200146": { "name": "Gideon", "fileName": "use_2_13.webp" }, 41 | "63200148": { "name": "Payla", "fileName": "use_2_13.webp" }, 42 | "63200158": { "name": "Lenora", "fileName": "use_2_13.webp" }, 43 | "63200164": { 44 | "name": "Great Castle Neria", 45 | "fileName": "use_2_13.webp" 46 | }, 47 | "63200165": { "name": "Piyer", "fileName": "use_2_13.webp" }, 48 | "63200170": { "name": "Levi", "fileName": "use_2_13.webp" }, 49 | "63200171": { "name": "Goulding", "fileName": "use_2_13.webp" }, 50 | "63200174": { "name": "Bergstrom", "fileName": "use_2_13.webp" }, 51 | "63200185": { "name": "Giant Worm", "fileName": "use_2_13.webp" }, 52 | "63200188": { "name": "Cadogan", "fileName": "use_2_13.webp" }, 53 | "63200191": { "name": "Berhart", "fileName": "use_2_13.webp" }, 54 | "63200192": { "name": "Brinewt", "fileName": "use_2_13.webp" }, 55 | "63200197": { "name": "Egg of Creation", "fileName": "use_2_13.webp" }, 56 | "63200205": { "name": "Sir Druden", "fileName": "use_2_13.webp" }, 57 | "63200206": { "name": "Sir Valleylead", "fileName": "use_2_13.webp" }, 58 | "63200209": { "name": "Javern", "fileName": "use_2_13.webp" }, 59 | "63200222": { "name": "Siera", "fileName": "use_2_13.webp" }, 60 | "63200226": { "name": "Morpheo", "fileName": "use_2_13.webp" }, 61 | "63200227": { "name": "Morina", "fileName": "use_2_13.webp" }, 62 | "63200228": { "name": "Madam Moonscent", "fileName": "use_2_13.webp" }, 63 | "63200305": { "name": "Albion", "fileName": "use_2_13.webp" }, 64 | "63200308": { "name": "Seto", "fileName": "use_2_13.webp" }, 65 | "63200309": { "name": "Cicerra", "fileName": "use_2_13.webp" }, 66 | "63200310": { "name": "Stella", "fileName": "use_2_13.webp" }, 67 | "70500000": { "name": "Fancier Boquet", "fileName": "all_quest_03_133.webp" }, 68 | "70500001": { 69 | "name": "Prideholme Potato", 70 | "fileName": "all_quest_01_108.webp" 71 | }, 72 | "70500002": { 73 | "name": "Rethramis Holy Water", 74 | "fileName": "all_quest_01_23.webp" 75 | }, 76 | "70500003": { 77 | "name": "Yudia Natural Salt", 78 | "fileName": "all_quest_01_64.webp" 79 | }, 80 | "70500006": { "name": "Stalwart Cage", "fileName": "all_quest_02_46.webp" }, 81 | "70500007": { 82 | "name": "Dyorika Straw Hat", 83 | "fileName": "all_quest_01_183.webp" 84 | }, 85 | "70500008": { 86 | "name": "Model of Luterra's Sword", 87 | "fileName": "all_quest_01_161.webp" 88 | }, 89 | "70500011": { "name": "Lakebar Tomato Juice", "fileName": "use_1_224.webp" }, 90 | "70500012": { 91 | "name": "Azenaporium Brooch", 92 | "fileName": "all_quest_01_57.webp" 93 | }, 94 | "70500013": { "name": "Black Rose", "fileName": "all_quest_02_95.webp" }, 95 | "70500014": { "name": "Round Glass Piece", "fileName": "use_3_129.webp" }, 96 | "70500015": { "name": "Mokoko Carrot", "fileName": "all_quest_01_172.webp" }, 97 | "70500016": { 98 | "name": "Oversize Ladybug Doll", 99 | "fileName": "all_quest_03_113.webp" 100 | }, 101 | "70500024": { "name": "Magick Cloth", "fileName": "all_quest_01_207.webp" }, 102 | "70500025": { "name": "Magick Crystal", "fileName": "all_quest_02_71.webp" }, 103 | "70500026": { 104 | "name": "Exquisite Music Box", 105 | "fileName": "all_quest_01_56.webp" 106 | }, 107 | "70500027": { 108 | "name": "Queen's Knights Application", 109 | "fileName": "all_quest_01_141.webp" 110 | }, 111 | "70500028": { "name": "Goblin Yarn", "fileName": "all_quest_01_105.webp" }, 112 | "70500033": { "name": "Soundstone of Dawn", "fileName": "use_6_10.webp" }, 113 | "70500034": { "name": "Elemental's Feather", "fileName": "use_6_11.webp" }, 114 | "70500037": { "name": "Fargar's Beer", "fileName": "use_6_84.webp" }, 115 | "70500038": { "name": "Broken Dagger", "fileName": "use_6_228.webp" }, 116 | "70500039": { "name": "Book of Survival", "fileName": "use_6_229.webp" }, 117 | "70500040": { 118 | "name": "Dessicated Wooden Statue", 119 | "fileName": "use_6_230.webp" 120 | }, 121 | "70500056": { "name": "Danube's Earrings", "fileName": "use_7_132.webp" }, 122 | "70500059": { "name": "Hollowfruit", "fileName": "use_7_134.webp" }, 123 | "70500060": { "name": "Piñata Crafting Set", "fileName": "use_7_135.webp" }, 124 | "70500061": { 125 | "name": "Rainbow Tikatika Flower", 126 | "fileName": "use_7_136.webp" 127 | }, 128 | "70501000": { "name": "Surprise Chest", "fileName": "all_quest_02_230.webp" }, 129 | "70501001": { 130 | "name": "Sky Reflection Oil", 131 | "fileName": "all_quest_01_117.webp" 132 | }, 133 | "70501002": { 134 | "name": "Chain War Chronicles", 135 | "fileName": "all_quest_01_155.webp" 136 | }, 137 | "70501003": { 138 | "name": "Shy Wind Flower Pollen", 139 | "fileName": "all_quest_01_66.webp" 140 | }, 141 | "70501004": { 142 | "name": "Angler's Fishing Pole", 143 | "fileName": "lifelevel_01_59.webp" 144 | }, 145 | "70501005": { "name": "Fine Gramophone", "fileName": "all_quest_01_90.webp" }, 146 | "70501006": { 147 | "name": "Vern's Founding Coin", 148 | "fileName": "all_quest_01_253.webp" 149 | }, 150 | "70501007": { 151 | "name": "Sirius's Holy Book", 152 | "fileName": "all_quest_03_4.webp" 153 | }, 154 | "70501008": { "name": "Red Moon's Tears", "fileName": "use_6_231.webp" }, 155 | "70501010": { 156 | "name": "Sylvain Queen's Blessing", 157 | "fileName": "use_7_133.webp" 158 | }, 159 | "70501011": { "name": "Oreha Viewing Stone", "fileName": "use_7_137.webp" }, 160 | "70503000": { 161 | "name": "Tournament Entrance Stamp", 162 | "fileName": "use_8_38.webp" 163 | }, 164 | "70503001": { "name": "Yudia Spellbook", "fileName": "use_8_39.webp" }, 165 | "70503005": { "name": "Shimmering Essence", "fileName": "use_8_41.webp" }, 166 | "70503006": { "name": "Energy X7 Capsule", "fileName": "use_8_42.webp" }, 167 | "70503008": { 168 | "name": "Piyer's Secret Textbook", 169 | "fileName": "use_8_44.webp" 170 | }, 171 | 172 | "70500063": { "name": "Feather Fan", "fileName": "use_9_210.webp" }, 173 | "70500062": { "name": "Febre Potion", "fileName": "use_9_209.webp" }, 174 | "70500064": { "name": "Mockup Firefly", "fileName": "use_9_211.webp" }, 175 | "70501012": { "name": "Necromancer's Records", "fileName": "use_9_212.webp" }, 176 | 177 | "63200407": { "name": "Satra", "fileName": "use_2_13.webp" }, 178 | "63200408": { "name": "Killian", "fileName": "use_2_13.webp" }, 179 | "63200402": { "name": "Lujean", "fileName": "use_2_13.webp" }, 180 | "63200404": { "name": "Vern Zenlord", "fileName": "use_2_13.webp" }, 181 | "63200403": { "name": "Xereon", "fileName": "use_2_13.webp" } 182 | } 183 | -------------------------------------------------------------------------------- /data/itemRarity.json: -------------------------------------------------------------------------------- 1 | { 2 | "70500000": 2, 3 | "70500001": 2, 4 | "70500002": 2, 5 | "70501000": 3, 6 | "63200095": 1, 7 | "63200094": 1, 8 | "63200222": 0, 9 | 10 | "70503001": 2, 11 | "70500003": 2, 12 | "70501001": 3, 13 | "63200185": 0, 14 | "63200227": 0, 15 | "63200102": 1, 16 | 17 | "70500013": 2, 18 | "70500011": 2, 19 | "70500006": 2, 20 | "63200191": 0, 21 | "63200188": 0, 22 | "63200106": 1, 23 | "921405": 2, 24 | 25 | "63200192": 0, 26 | "63200226": 0, 27 | "63200104": 1, 28 | "931505": 1, 29 | 30 | "70500012": 2, 31 | "70500007": 2, 32 | "70500008": 2, 33 | "70501002": 3, 34 | "63200111": 1, 35 | "63200118": 1, 36 | "63200024": 2, 37 | "931512": 1, 38 | 39 | "70500015": 2, 40 | "70500016": 2, 41 | "70500014": 2, 42 | "70501003": 3, 43 | "63200197": 0, 44 | "63200122": 1, 45 | "63200030": 2, 46 | 47 | "70503000": 2, 48 | "70501004": 3, 49 | "63200206": 0, 50 | "63200205": 0, 51 | "63200228": 0, 52 | "63200008": 3, 53 | 54 | "70501005": 3, 55 | "70503006": 2, 56 | "63200174": 1, 57 | "63200136": 1, 58 | "63200035": 2, 59 | "920404": 1, 60 | 61 | "70500026": 2, 62 | "70500028": 2, 63 | "70500024": 2, 64 | "70500025": 2, 65 | "70500027": 2, 66 | "70501006": 3, 67 | "63200148": 1, 68 | "63200146": 1, 69 | "63200039": 2, 70 | 71 | "70503005": 2, 72 | "70501007": 3, 73 | "63200209": 0, 74 | "63200144": 1, 75 | "63200038": 2, 76 | "930605": 1, 77 | 78 | "70500056": 2, 79 | "70500034": 2, 80 | "70500033": 2, 81 | "70501010": 3, 82 | "63200108": 1, 83 | "63200158": 1, 84 | "63200050": 2, 85 | "930902": 2, 86 | 87 | "70503008": 2, 88 | "70500037": 3, 89 | "63200164": 1, 90 | "63200165": 1, 91 | "63200053": 2, 92 | "920701": 0, 93 | 94 | "70500040": 2, 95 | "70500038": 2, 96 | "70500039": 2, 97 | "70501008": 3, 98 | "63200171": 1, 99 | "63200170": 1, 100 | "63200057": 2, 101 | "921004": 1, 102 | 103 | "70500060": 2, 104 | "70500059": 2, 105 | "70500061": 2, 106 | "70501011": 3, 107 | "63200308": 1, 108 | "63200310": 1, 109 | "63200309": 1, 110 | "63200305": 2, 111 | "920803": 1, 112 | 113 | "70500063": 2, 114 | "70500062": 2, 115 | "70500064": 2, 116 | "70501012": 3, 117 | 118 | "63200407": 0, 119 | "63200408": 0, 120 | "63200402": 1, 121 | "63200404": 1, 122 | "63200403": 1 123 | } 124 | -------------------------------------------------------------------------------- /data/merchantSchedules.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": [ 3 | { "h": 1, "m": 30 }, 4 | { "h": 4, "m": 30 }, 5 | { "h": 5, "m": 30 }, 6 | { "h": 7, "m": 30 }, 7 | { "h": 8, "m": 30 }, 8 | { "h": 11, "m": 30 }, 9 | { "h": 13, "m": 30 }, 10 | { "h": 16, "m": 30 }, 11 | { "h": 17, "m": 30 }, 12 | { "h": 19, "m": 30 }, 13 | { "h": 20, "m": 30 }, 14 | { "h": 23, "m": 30 } 15 | ], 16 | "2": [ 17 | { "h": 0, "m": 30 }, 18 | { "h": 2, "m": 30 }, 19 | { "h": 5, "m": 30 }, 20 | { "h": 6, "m": 30 }, 21 | { "h": 8, "m": 30 }, 22 | { "h": 9, "m": 30 }, 23 | { "h": 12, "m": 30 }, 24 | { "h": 14, "m": 30 }, 25 | { "h": 17, "m": 30 }, 26 | { "h": 18, "m": 30 }, 27 | { "h": 20, "m": 30 }, 28 | { "h": 21, "m": 30 } 29 | ], 30 | "3": [ 31 | { "h": 0, "m": 30 }, 32 | { "h": 3, "m": 30 }, 33 | { "h": 4, "m": 30 }, 34 | { "h": 6, "m": 30 }, 35 | { "h": 7, "m": 30 }, 36 | { "h": 10, "m": 30 }, 37 | { "h": 12, "m": 30 }, 38 | { "h": 15, "m": 30 }, 39 | { "h": 16, "m": 30 }, 40 | { "h": 18, "m": 30 }, 41 | { "h": 19, "m": 30 }, 42 | { "h": 22, "m": 30 } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /data/merchants.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Ben", 4 | "continent": "Rethramis", 5 | "locations": { 6 | "Ankumo Mountain": "yvFAKTY.png", 7 | "Log Hill": "CL4zHE3.png", 8 | "Rethramis Border": "W0v3Rv1.png" 9 | }, 10 | "schedule": 3, 11 | "items": { 12 | "rapport": [70500000, 70500001, 70500002, 70501000], 13 | "cards": [63200095, 63200094, 63200222], 14 | "cooking": [] 15 | } 16 | }, 17 | { 18 | "name": "Lucas", 19 | "continent": "Yudia", 20 | "locations": { 21 | "Ozhorn Hill": "2suO3M1.png", 22 | "Saland Hill": "32mF4gP.png" 23 | }, 24 | "schedule": 1, 25 | "items": { 26 | "rapport": [70503001, 70500003, 70501001], 27 | "cards": [63200185, 63200227, 63200102], 28 | "cooking": [] 29 | } 30 | }, 31 | { 32 | "name": "Malone", 33 | "continent": "West Luterra", 34 | "locations": { 35 | "Battlebound Plains": "waIAYl0.png", 36 | "Bilbrin Forest": "9zXOVIH.png", 37 | "Lakebar": "KB40JN3.png", 38 | "Medrick Monastery": "jCa7R1i.png", 39 | "Mount Zagoras": "M76MErL.png" 40 | }, 41 | "schedule": 2, 42 | "items": { 43 | "rapport": [70500013, 70500011, 70500006, 70501002], 44 | "cards": [63200191, 63200188, 63200106], 45 | "cooking": [921405] 46 | } 47 | }, 48 | { 49 | "name": "Morris", 50 | "continent": "East Luterra", 51 | "locations": { 52 | "Dyorika Plain": "GSECxpe.png", 53 | "Flowering Orchard": "oqTzPl3.png", 54 | "Sunbright Hill": "tC2tklW.png" 55 | }, 56 | "schedule": 1, 57 | "items": { 58 | "rapport": [70500012, 70500007, 70500008, 70501002], 59 | "cards": [63200192, 63200226, 63200104, 63200024], 60 | "cooking": [931505] 61 | } 62 | }, 63 | { 64 | "name": "Burt", 65 | "continent": "East Luterra", 66 | "locations": { 67 | "Blackrose Chapel": "9f2phQi.png", 68 | "Boreas Domain": "zVtnPxO.png", 69 | "Croconys Seashore North": "QATQXGj.png", 70 | "Croconys Seashore South": "f1En5HM.png", 71 | "Leyar Terrace": "ZM5Nt76.png" 72 | }, 73 | "schedule": 2, 74 | "items": { 75 | "rapport": [70500012, 70500007, 70500008, 70501002], 76 | "cards": [63200111, 63200118, 63200024], 77 | "cooking": [931512] 78 | } 79 | }, 80 | { 81 | "name": "Oliver", 82 | "continent": "Tortoyk", 83 | "locations": { 84 | "Forest of Giants": "syPpeL5.png", 85 | "Seaswept Woods": "hI0Vezm.png", 86 | "Skyreach Steppe": "x64d9w3.png", 87 | "Sweetwater Forest": "aHDyBpo.png" 88 | }, 89 | "schedule": 2, 90 | "items": { 91 | "rapport": [70500015, 70500016, 70500014, 70501003], 92 | "cards": [63200197, 63200122, 63200030], 93 | "cooking": [] 94 | } 95 | }, 96 | { 97 | "name": "Mac", 98 | "continent": "Anikka", 99 | "locations": { 100 | "Delphi Township": "ure8D2G.png", 101 | "Melody Forest": "mirR1MN.png", 102 | "Prisma Valley": "DT9pMA2.png", 103 | "Rattan Hill": "SsV57qW.png", 104 | "Twilight Mists": "DChixrV.png" 105 | }, 106 | "schedule": 1, 107 | "items": { 108 | "rapport": [70503000, 70501004], 109 | "cards": [63200206, 63200205, 63200228, 63200008], 110 | "cooking": [] 111 | } 112 | }, 113 | { 114 | "name": "Nox", 115 | "continent": "Arthetine", 116 | "locations": { 117 | "Arid Path": "HXtkwg3.png", 118 | "Nebelhorn": "1z8DpSK.png", 119 | "Riza Falls": "gErMCEi.png", 120 | "Scraplands": "fhR7bvF.png", 121 | "Totrich": "MHmE0lW.png", 122 | "Windbringer Hill": "Kwnt0uN.png" 123 | }, 124 | "schedule": 2, 125 | "items": { 126 | "rapport": [70503006, 70501005], 127 | "cards": [63200174, 63200136, 63200035], 128 | "cooking": [920404] 129 | } 130 | }, 131 | { 132 | "name": "Peter", 133 | "continent": "North Vern", 134 | "locations": { 135 | "Balankar Mountains": "PahvMKB.png", 136 | "Fesnar Highland": "IQ34OqG.png", 137 | "Parna Forest": "thufgcH.png", 138 | "Port Krona": "oE1L9kV.png", 139 | "Vernese Forest": "oAQRo61.png" 140 | }, 141 | "schedule": 3, 142 | "items": { 143 | "rapport": [70500026, 70500028, 70500024, 70500025, 70500027, 70501006], 144 | "cards": [63200148, 63200146, 63200039], 145 | "cooking": [] 146 | } 147 | }, 148 | { 149 | "name": "Jeffrey", 150 | "continent": "Shushire", 151 | "locations": { 152 | "Bitterwind Hill": "7JV4gol.png", 153 | "Frozen Sea": "P6s2yVy.png", 154 | "Iceblood Plateau": "cjle1Ej.png", 155 | "Icewing Heights": "bZYrlmi.png", 156 | "Lake Eternity": "0enqUWE.png" 157 | }, 158 | "schedule": 1, 159 | "items": { 160 | "rapport": [70503005, 70501007], 161 | "cards": [63200209, 63200144, 63200038], 162 | "cooking": [930605] 163 | } 164 | }, 165 | { 166 | "name": "Aricer", 167 | "continent": "Rohendel", 168 | "locations": { 169 | "Breezesome Brae": "GYV2jEK.png", 170 | "Elzowins Shade": "VP1zYx0.png", 171 | "Glass Lotus Lake": "FZJjRf4.png", 172 | "Lake Shiverwave": "kRuC2GT.png", 173 | "Xeneela Ruins": "D2o5eDU.png" 174 | }, 175 | "schedule": 2, 176 | "items": { 177 | "rapport": [70500056, 70500034, 70500033, 70501010], 178 | "cards": [63200108, 63200158, 63200050], 179 | "cooking": [930902] 180 | } 181 | }, 182 | { 183 | "name": "Laitir", 184 | "continent": "Yorn", 185 | "locations": { 186 | "Black Anvil Mine": "3NMX3tZ.png", 187 | "Hall of Promise": "CYINka2.png", 188 | "Iron Hammer Mine": "17PcN7E.png", 189 | "Unfinished Garden": "wrg1bmq.png", 190 | "Yorn's Cradle": "vVnvyG9.png" 191 | }, 192 | "schedule": 3, 193 | "items": { 194 | "rapport": [70503008, 70500037], 195 | "cards": [63200164, 63200165, 63200053], 196 | "cooking": [920701] 197 | } 198 | }, 199 | { 200 | "name": "Dorella", 201 | "continent": "Feiton", 202 | "locations": { 203 | "Kalaja": "ajlrAvm.png" 204 | }, 205 | "schedule": 1, 206 | "items": { 207 | "rapport": [70500040, 70500038, 70500039, 70501008], 208 | "cards": [63200171, 63200170, 63200057], 209 | "cooking": [921004] 210 | } 211 | }, 212 | { 213 | "name": "Rayni", 214 | "continent": "Punika", 215 | "locations": { 216 | "Tideshelf Path": "k47Nxtg.png", 217 | "Starsand Beach": "wZhsbu0.png", 218 | "Tikatika Colony": "GUpNFlg.png", 219 | "Secret Forest": "K5cVDTh.png" 220 | }, 221 | "schedule": 2, 222 | "items": { 223 | "rapport": [70500060, 70500059, 70500061, 70501011], 224 | "cards": [63200308, 63200310, 63200309, 63200305], 225 | "cooking": [920803] 226 | } 227 | }, 228 | { 229 | "name": "Evan", 230 | "continent": "South Vern", 231 | "locations": { 232 | "Candaria Territory": "Lk5fSpk.png", 233 | "Bellion Ruins": "wv9ZpK4.png" 234 | }, 235 | "schedule": 3, 236 | "items": { 237 | "rapport": [70500063, 70500062, 70500064, 70501012], 238 | "cards": [63200407, 63200408, 63200402, 63200404, 63200403], 239 | "cooking": [] 240 | } 241 | } 242 | ] 243 | -------------------------------------------------------------------------------- /data/msgs.json: -------------------------------------------------------------------------------- 1 | [[["Fever Time","icon_calendar_event_0.webp"],["Siege Content","icon_calendar_event_1.webp"],["Chaos Gate","icon_calendar_event_2.webp"],["Field Boss","icon_calendar_event_3.webp"],["Adventure Island","icon_calendar_event_4.webp"],["Ghost Ship","icon_calendar_event_5.webp"],["Islands","icon_calendar_event_6.webp"],["Sailing","icon_calendar_event_7.webp"],["Rowen","icon_calendar_event_8.webp"],["Siege","icon_calendar_event_9.webp"],["Proving Grounds","icon_calendar_event_10.webp"]],["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]] -------------------------------------------------------------------------------- /data/regions.json: -------------------------------------------------------------------------------- 1 | { 2 | "US West": [ 3 | "Mari", 4 | "Valtan", 5 | "Enviska", 6 | "Akkan", 7 | "Bergstrom", 8 | "Shandi", 9 | "Rohendel" 10 | ], 11 | "US East": [ 12 | "Azena", 13 | "Una", 14 | "Regulus", 15 | "Avesta", 16 | "Galatur", 17 | "Karta", 18 | "Ladon", 19 | "Kharmine", 20 | "Elzowin", 21 | "Sasha", 22 | "Adrinne", 23 | "Aldebaran", 24 | "Zosma", 25 | "Vykas", 26 | "Danube" 27 | ], 28 | "EU Central": [ 29 | "Neria", 30 | "Kadan", 31 | "Trixion", 32 | "Calvasus", 33 | "Thirain", 34 | "Zinnervale", 35 | "Asta", 36 | "Wei", 37 | "Slen", 38 | "Sceptrum", 39 | "Procyon", 40 | "Beatrice", 41 | "Inanna", 42 | "Thaemine", 43 | "Sirius", 44 | "Antares", 45 | "Brelshaza", 46 | "Nineveh", 47 | "Mokoko" 48 | ], 49 | "EU West": [ 50 | "Rethramis", 51 | "Tortoyk", 52 | "Moonkeep", 53 | "Stonehearth", 54 | "Shadespire", 55 | "Tragon", 56 | "Petrania", 57 | "Punika" 58 | ], 59 | "South America": [ 60 | "Kazeros", 61 | "Agaton", 62 | "Gienah", 63 | "Arcturus", 64 | "Yorn", 65 | "Feiton", 66 | "Vern", 67 | "Kurzan", 68 | "Prideholme" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | i18n: { 3 | defaultLocale: 'en', 4 | locales: ['en', 'zh'], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const { i18n } = require('./next-i18next.config') 3 | module.exports = { 4 | webpack(config, options) { 5 | config.module.rules.push({ 6 | test: /\.(mp3)$/, 7 | type: 'asset/resource', 8 | generator: { 9 | filename: 'static/chunks/[path][name].[hash][ext]', 10 | }, 11 | }) 12 | return config 13 | }, 14 | reactStrictMode: true, 15 | images: { domains: ['lostarkcodex.com'] }, 16 | async redirects() { 17 | return [ 18 | { 19 | source: '/', 20 | destination: '/alarms', 21 | permanent: true, 22 | }, 23 | ] 24 | }, 25 | i18n, 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "@olerichter00/use-localstorage": "^0.1.1", 10 | "@tabler/icons": "^1.55.0", 11 | "@types/howler": "^2.2.6", 12 | "@types/luxon": "^2.3.0", 13 | "classnames": "^2.3.1", 14 | "core-js": "^3.21.1", 15 | "daisyui": "^2.8.0", 16 | "howler": "^2.2.3", 17 | "luxon": "^2.3.1", 18 | "next": "latest", 19 | "next-i18next": "^11.0.0", 20 | "react": "^17.0.2", 21 | "react-dom": "^17.0.2", 22 | "socket.io-client": "^4.4.1", 23 | "swr": "^1.2.2", 24 | "uuid": "^8.3.2" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "17.0.4", 28 | "@types/react": "17.0.38", 29 | "@types/uuid": "^8.3.4", 30 | "autoprefixer": "^10.4.0", 31 | "postcss": "^8.4.5", 32 | "prettier": "^2.5.1", 33 | "prettier-plugin-tailwindcss": "^0.1.1", 34 | "tailwindcss": "^3.0.7", 35 | "typescript": "4.5.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import Head from 'next/head' 4 | import Script from 'next/script' 5 | import { ChangeLogModal, GitHubModal, SideBar } from '../components' 6 | import { appWithTranslation } from 'next-i18next' 7 | import { IconBrandTwitch } from '@tabler/icons' 8 | import { SWRConfig } from 'swr' 9 | import NavBar from '../components/NavBar' 10 | 11 | function MyApp({ Component, pageProps, ...AppProps }: AppProps) { 12 | return ( 13 | <> 14 | 15 | Lost Ark Timer 16 | 17 | 21 | 22 | 23 | 24 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | fetch(resource, init).then((res) => res.json()), 50 | }} 51 | > 52 | 53 | 54 | 66 |
67 | {process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' ? ( 68 | {' '} 97 | 98 | ) : null} 99 | 100 | ) 101 | } 102 | export default appWithTranslation(MyApp) 103 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | } from 'next/document' 8 | 9 | class MyDocument extends Document { 10 | static async getInitialProps(ctx: DocumentContext) { 11 | const initialProps = await Document.getInitialProps(ctx) 12 | 13 | return initialProps 14 | } 15 | render() { 16 | return ( 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default MyDocument 29 | -------------------------------------------------------------------------------- /pages/alarms.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import React, { useState, useEffect, useRef } from 'react' 4 | import { APIGameEvent, APIEventType } from '../common/api' 5 | import { GameEvent } from '../common' 6 | import { AlarmConfigModal } from '../components' 7 | import { DateTime, Duration, Interval } from 'luxon' 8 | import useLocalStorage from '@olerichter00/use-localstorage' 9 | import { Howl, Howler } from 'howler' 10 | import { alert1, alert2, alert3, alert4, alert5, alert6 } from '../sounds' 11 | import 'core-js/features/array/at' 12 | import { IconSettings } from '@tabler/icons' 13 | import usePrevious from '../util/usePrevious' 14 | import { createTableData } from '../util/createTableData' 15 | import { RegionKey } from '../util/types/types' 16 | import { RegionTimeZoneMapping } from '../util/static' 17 | import { useTranslation } from 'next-i18next' 18 | var classNames = require('classnames') 19 | 20 | type AlertSoundKeys = 21 | | 'Alert 1' 22 | | 'Alert 2' 23 | | 'Alert 3' 24 | | 'Alert 4' 25 | | 'Alert 5' 26 | | 'Alert 6' 27 | type eventName = string 28 | type iconUrl = string 29 | type iLvlInt = number 30 | type eventId = string 31 | 32 | type EventIdMapping = [ 33 | id: eventId, 34 | mapping: [name: eventName, icon: iconUrl, iLvl: iLvlInt] 35 | ] 36 | type EventTypeIconMapping = [eventType: string, eventIconUrl: string] 37 | 38 | const eventIDNameMapping: Array = Object.entries( 39 | require('../data/events.json') 40 | ).map((e) => { 41 | const [id, [name, url, iLvl]] = e as EventIdMapping 42 | return new APIGameEvent(id, name, url, iLvl) 43 | }) 44 | 45 | const groupedEvents = { 46 | 'Arkesia Grand Prix': eventIDNameMapping 47 | .filter(({ name }) => name.includes('Grand Prix')) 48 | .map((e) => e.id), 49 | 'Field Bosses': eventIDNameMapping 50 | .filter(({ iconUrl }) => iconUrl === 'achieve_14_142.webp') 51 | .map((e) => e.id), 52 | 'Chaos Gates': eventIDNameMapping 53 | .filter(({ iconUrl }) => iconUrl === 'achieve_13_11.webp') 54 | .sort((a, b) => b.minItemLevel - a.minItemLevel) // this is a bit of a hack to use one of the hourly gates as the canonical one 55 | .map((e) => e.id), 56 | 'Ghost Ships': eventIDNameMapping 57 | .filter(({ name }) => name.includes('Ghost Ship')) 58 | .map((e) => e.id), 59 | } 60 | 61 | const eventTypeIconMapping: Array = Object.entries( 62 | require('../data/msgs.json')[0] 63 | ).map(([idx, e]) => { 64 | const [name, url] = e as EventTypeIconMapping 65 | return new APIEventType(Number(idx), name, url) 66 | }) 67 | const allEventData = require('../data/data.json') 68 | const sounds = { 69 | 'Alert 1': alert1, 70 | 'Alert 2': alert2, 71 | 'Alert 3': alert3, 72 | 'Alert 4': alert4, 73 | 'Alert 5': alert5, 74 | 'Alert 6': alert6, 75 | } 76 | 77 | const Alarms: NextPage = () => { 78 | const { t } = useTranslation('events') 79 | const [currDate, setCurrDate] = useState(DateTime.now()) 80 | const [regionTZ, setRegionTZ] = useLocalStorage( 81 | 'regionTZ', 82 | RegionTimeZoneMapping['US West'] 83 | ) 84 | const [regionTZName, setRegionTZName] = useLocalStorage( 85 | 'regionTZName', 86 | 'US West' 87 | ) 88 | const isMounted = useRef(false) 89 | const defaultTheme = () => { 90 | // Defaults to system theme if unconfigured 91 | return ( 92 | localStorage.getItem('darkMode') || 93 | (window.matchMedia && 94 | window.matchMedia('(prefers-color-scheme: dark)').matches) 95 | ) 96 | } 97 | const [darkMode, setDarkMode] = useLocalStorage( 98 | 'darkMode', 99 | defaultTheme 100 | ) 101 | useEffect(() => { 102 | //Prevents FoUC (Flash of Unstylized Content) by not refreshing on first mount 103 | if (!isMounted.current) { 104 | isMounted.current = true 105 | return 106 | } 107 | 108 | //Toggle Daisy UI colors (e.g. bg-base-###) 109 | document.documentElement.setAttribute( 110 | 'data-theme', 111 | darkMode ? 'dark' : 'light' 112 | ) 113 | 114 | //Toggle standard Tailwind colors (e.g. bg-sky-800) 115 | darkMode 116 | ? document.documentElement.classList.add('dark') 117 | : document.documentElement.classList.remove('dark') 118 | }, [darkMode]) 119 | 120 | const [serverTime, setServerTime] = useState( 121 | currDate.setZone(regionTZ) 122 | ) 123 | const [selectedDate, setSelectedDate] = useState(currDate.setZone(regionTZ)) 124 | 125 | const [gameEvents, setGameEvents] = useState | undefined>( 126 | undefined 127 | ) 128 | const [todayEvents, setTodayEvents] = useState>([]) 129 | const [fullEventsTable, setFullEventsTable] = useState>([]) 130 | const [currentEventsTable, setCurrentEventsTable] = useState< 131 | Array 132 | >([]) 133 | 134 | const [selectedEventType, setSelectedEventType] = useState(-1) 135 | const [viewLocalizedTime, setViewLocalizedTime] = useLocalStorage( 136 | 'viewLocalizedTime', 137 | true 138 | ) 139 | const [view24HrTime, setView24HrTime] = useLocalStorage( 140 | 'view24HrTime', 141 | false 142 | ) 143 | const [notifyInMins, setNotifyInMins] = useLocalStorage( 144 | 'notifyInMins', 145 | 15 146 | ) 147 | const [alertSound, setAlertSound] = useLocalStorage( 148 | 'alertSound', 149 | 'muted' 150 | ) 151 | const [disabledAlarms, setDisabledAlarms] = useLocalStorage<{ 152 | [key: string]: number 153 | }>('disabledAlarms', {}) 154 | const [desktopNotifications, setDesktopNotifications] = 155 | useLocalStorage('desktopNotifications', false) 156 | const [hideGrandPrix, setHideGrandPrix] = useLocalStorage( 157 | 'hideGrandPrix', 158 | false 159 | ) 160 | const [unlockedAudio, setUnlockedAudio] = useState(false) 161 | const [moveDisabledEventsBottom, setMoveDisabledEventsBottom] = 162 | useLocalStorage('moveDisabledEventsBottom', false) 163 | const [hideDisabledEvents, setHideDisableEvents] = useLocalStorage( 164 | 'hideDisabledEvents', 165 | false 166 | ) 167 | const [mounted, setMounted] = useState(false) 168 | const [volume, setVolume] = useLocalStorage('volume', 0.4) 169 | const buttons = [ 170 | useRef(null), 171 | useRef(null), 172 | useRef(null), 173 | useRef(null), 174 | useRef(null), 175 | useRef(null), 176 | useRef(null), 177 | useRef(null), 178 | useRef(null), 179 | useRef(null), 180 | useRef(null), 181 | useRef(null), 182 | ] 183 | 184 | useEffect(() => { 185 | if (regionTZ !== undefined) { 186 | setMounted(true) 187 | setServerTime(currDate.setZone(regionTZ)) 188 | setSelectedDate(currDate.setZone(regionTZ)) 189 | } 190 | }, [regionTZ]) 191 | 192 | useEffect(() => { 193 | if (volume !== undefined) Howler.volume(volume) 194 | }, [volume]) 195 | 196 | useEffect(() => { 197 | setMounted(true) 198 | Howler.autoSuspend = false 199 | }, []) 200 | useEffect(() => { 201 | const timer = setInterval(() => { 202 | let now = DateTime.now() 203 | if (currDate.endOf('day').diffNow().toMillis() < 0) setSelectedDate(now) 204 | setCurrDate(now) 205 | setServerTime(now.setZone(regionTZ)) 206 | }, 1000) 207 | return () => { 208 | clearInterval(timer) // Return a function to clear the timer so that it will stop being called on unmount 209 | } 210 | }, [regionTZ, view24HrTime, viewLocalizedTime, selectedDate]) 211 | 212 | // clear disabled alarm when alarm expires 213 | useEffect(() => { 214 | if (disabledAlarms) { 215 | let keys = Object.keys(disabledAlarms) 216 | keys.forEach((key) => { 217 | if (disabledAlarms[key] < DateTime.now().toMillis()) { 218 | delete disabledAlarms[key] 219 | } 220 | }) 221 | setDisabledAlarms(disabledAlarms) 222 | } 223 | }, [serverTime.minute]) 224 | // read and populate all game events 225 | useEffect(() => { 226 | if (!mounted || regionTZ === undefined) return 227 | let gameEvents: Array = [] 228 | let disabledAlarmsKeys = Object.keys(disabledAlarms || {}) 229 | Object.entries(allEventData).forEach((eventType) => { 230 | const [type, monthDayMap] = eventType as [string, any] 231 | let et = eventTypeIconMapping.find((et) => et.id.toString() === type)! 232 | for (const [month, days] of Object.entries(monthDayMap) as [ 233 | string, 234 | any 235 | ]) { 236 | for (const [day, events] of Object.entries(days) as [string, any]) { 237 | for (const [iLvl, event] of Object.entries(events) as [string, any]) { 238 | for (const [eventId, eventTime] of Object.entries(event) as [ 239 | string, 240 | any 241 | ]) { 242 | let gt = eventIDNameMapping.find((gt) => gt.id === eventId)! 243 | 244 | let gameEvent = new GameEvent(et, gt) 245 | eventTime.forEach((time: string, idx: number) => { 246 | const [startTime, endTime] = time.split('-') 247 | const [startHr, startMin] = startTime.split(':') 248 | const [endHr, endMin] = endTime?.split(':') ?? ['', ''] 249 | let start = DateTime.fromObject( 250 | { 251 | year: currDate.year, 252 | month: Number(month), 253 | day: Number(day), 254 | hour: Number(startHr), 255 | minute: Number(startMin), 256 | }, 257 | { zone: regionTZ } 258 | ) 259 | let id = Number(gt.id) 260 | if ( 261 | (7000 <= id && id < 8000 && ![7013, 7035].includes(id)) || 262 | [ 263 | 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 264 | 5002, 5003, 5004, 5005, 6007, 6008, 6009, 6010, 6011, 265 | ].includes(id) 266 | ) { 267 | start = start.plus({ minutes: 10 }) 268 | } 269 | let end = DateTime.fromObject( 270 | { 271 | year: start.year, 272 | month: start.month, 273 | day: start.day, 274 | hour: Number(endHr != '' ? endHr : start.hour), 275 | minute: Number(endMin != '' ? endMin : start.minute), 276 | }, 277 | { zone: regionTZ } 278 | ) 279 | 280 | gameEvent.addTime(Interval.fromDateTimes(start, end)) 281 | if ( 282 | disabledAlarmsKeys.includes(gameEvent.gameEvent.id) && 283 | disabledAlarms 284 | ) 285 | gameEvent.disabled = 286 | DateTime.fromMillis( 287 | disabledAlarms[gameEvent.gameEvent.id] 288 | ) || null 289 | }) 290 | gameEvents.push(gameEvent) 291 | } 292 | } 293 | } 294 | } 295 | }) 296 | 297 | const todayEvents = gameEvents.filter( 298 | (ge) => 299 | ge.times.find((t) => { 300 | return t.start && t.start.day === selectedDate.day 301 | }) !== undefined && 302 | ge.times.length && 303 | ge.times.at(0)?.start.day === selectedDate.day && 304 | (ge.times.at(-1)?.start.day === selectedDate.plus({ days: 1 }).day || 305 | ge.times.at(-1)?.start.day === selectedDate.day) 306 | ) 307 | 308 | setGameEvents(gameEvents) 309 | setTodayEvents(todayEvents) 310 | }, [regionTZ, selectedDate, viewLocalizedTime, view24HrTime]) 311 | 312 | // (re)generate full events table and current events table on dependency array change (mostly config changes) 313 | useEffect(() => { 314 | if (mounted) { 315 | generateEventsTable(selectedEventType) 316 | } 317 | }, [ 318 | serverTime.minute, 319 | notifyInMins, 320 | disabledAlarms, 321 | hideGrandPrix, 322 | moveDisabledEventsBottom, 323 | hideDisabledEvents, 324 | todayEvents, 325 | selectedEventType, 326 | ]) 327 | // game event button click (filters events by type) 328 | const buttonClick = ( 329 | event: React.MouseEvent, 330 | id: number 331 | ) => { 332 | buttons.forEach((b) => { 333 | if (b.current) 334 | (b.current as unknown as Element).classList.remove('btn-active') 335 | }) 336 | let button = event.target as Element 337 | button.classList.add('btn-active') 338 | setSelectedEventType(id) 339 | // generateFullEventsTable(id) 340 | } 341 | 342 | const generateEventsTable = (eventType: number) => { 343 | // let allEvents: Array = [] 344 | let ms = Duration.fromObject({ minutes: notifyInMins }).toMillis() 345 | let disabledAlarmsKeys = Object.keys(disabledAlarms || {}) 346 | 347 | let currEventsTable: Array = [] 348 | let allEventsTable: Array = [] 349 | 350 | for (let i = 0; i < todayEvents?.length; i++) { 351 | let event = todayEvents[i] 352 | if (eventType !== -1 && event.eventType.id !== eventType) continue 353 | if (disabledAlarmsKeys.includes(event.gameEvent.id) && disabledAlarms) 354 | event.disabled = 355 | DateTime.fromMillis(disabledAlarms[event.gameEvent.id]) || null 356 | else event.disabled = null 357 | 358 | if (event.disabled && hideDisabledEvents) continue 359 | else if (event.disabled && moveDisabledEventsBottom) { 360 | allEventsTable.push(event) 361 | continue 362 | } 363 | 364 | if (hideGrandPrix) { 365 | const group = Object.entries(groupedEvents) 366 | .map(([name, ids]) => ({ 367 | idx: ids.indexOf(event.gameEvent.id), 368 | name, 369 | })) 370 | .filter(({ idx }) => idx >= 0)[0] 371 | if (group) { 372 | if (group.idx > 0) continue 373 | event.groupName = group.name 374 | } 375 | } 376 | 377 | let latest = event.latest(serverTime) 378 | if (latest) { 379 | let value = latest.start.diff(serverTime).valueOf() 380 | if (!event.disabled && 0 <= value && value <= ms) 381 | currEventsTable.push(event) 382 | else allEventsTable.push(event) 383 | } else { 384 | allEventsTable.push(event) 385 | } 386 | } 387 | 388 | allEventsTable = allEventsTable.sort((a, b) => { 389 | if (moveDisabledEventsBottom) { 390 | if (a.disabled) return Number.POSITIVE_INFINITY 391 | else if (b.disabled) return Number.NEGATIVE_INFINITY 392 | } 393 | 394 | let finalCmp = 0 395 | let aTime = a.latest(serverTime) 396 | let bTime = b.latest(serverTime) 397 | if (aTime && bTime) { 398 | let aTime = a.latest(serverTime).start.diff(serverTime).valueOf() 399 | let bTime = b.latest(serverTime).start.diff(serverTime).valueOf() 400 | 401 | if (aTime < bTime) { 402 | finalCmp = -1 403 | } else if (aTime - bTime < 1000) { 404 | finalCmp = a.gameEvent.minItemLevel - b.gameEvent.minItemLevel 405 | } else { 406 | finalCmp = 1 407 | } 408 | } else if (aTime) { 409 | finalCmp = -1 410 | } else if (bTime) { 411 | finalCmp = 1 412 | } else { 413 | finalCmp = a.gameEvent.minItemLevel - b.gameEvent.minItemLevel 414 | } 415 | return finalCmp 416 | }) 417 | currEventsTable = currEventsTable.sort( 418 | (a, b) => 419 | a.latest(serverTime).start.valueOf() - 420 | b.latest(serverTime).start.valueOf() 421 | ) 422 | const currentEventsTableData = createTableData({ 423 | events: currEventsTable, 424 | serverTime, 425 | currDate, 426 | viewLocalizedTime: viewLocalizedTime || false, 427 | view24HrTime: view24HrTime || false, 428 | isGameEvent: true, 429 | }) 430 | 431 | if ( 432 | currentEventsTableData.length > 0 && 433 | currentEventsTableData.length > currentEventsTable.length && 434 | (currentEventsTable.length !== 0 || 435 | currentEventsTableData !== currentEventsTable) 436 | ) { 437 | if (alertSound && alertSound !== 'muted') { 438 | let s = new Howl({ 439 | src: sounds[alertSound as AlertSoundKeys] as unknown as string, 440 | onunlock: (id) => setUnlockedAudio(true), 441 | }) 442 | s.play() 443 | } 444 | if (desktopNotifications) { 445 | let notification = new Notification( 446 | `${t('alarms:notification.heading', { notifyInMins })}`, 447 | { 448 | body: currEventsTable 449 | .map((e) => t(`${e.gameEvent.id}`)) 450 | .reduce((acc, curr, currIndex) => { 451 | if (currIndex < 3) { 452 | return `${acc}\n${curr}` 453 | } else if (currIndex === 3) { 454 | const additionalEvents: number = currEventsTable.length - 3 455 | return `${acc}\n${t('alarms:notification.additional-events', { 456 | additionalEvents, 457 | })}` 458 | } else { 459 | return acc 460 | } 461 | }, ''), 462 | icon: '/images/LA_Mokko_Seed.png', 463 | } 464 | ) 465 | notification.onclick = () => { 466 | window.focus() 467 | notification.close() 468 | } 469 | } 470 | } 471 | 472 | setFullEventsTable( 473 | createTableData({ 474 | events: allEventsTable, 475 | serverTime, 476 | currDate, 477 | viewLocalizedTime: viewLocalizedTime || false, 478 | view24HrTime: view24HrTime || false, 479 | isGameEvent: true, 480 | }) 481 | ) 482 | setCurrentEventsTable(currentEventsTableData) 483 | } 484 | 485 | // return react text node for game type buttons [disabled / all events] 486 | const eventsInSection = (eventId: number) => { 487 | let allEvents = 488 | todayEvents?.filter((te) => 489 | eventId === -1 ? te.eventType.id >= 0 : te.eventType.id === eventId 490 | ) || [] 491 | 492 | let remaining = allEvents?.filter((e) => !e.disabled) || [] 493 | 494 | if (remaining.length != allEvents.length) 495 | return ( 496 | <> 497 | {`${remaining.length}`} / {`${allEvents.length}`} 498 | 499 | ) 500 | return <>{allEvents.length} 501 | } 502 | 503 | return ( 504 | <> 505 | 506 | Alarms - Lost Ark Timer 507 | 511 | 512 | 518 |
519 |
520 |
521 | 540 | 553 | 572 | 578 |
579 |
580 |
581 | 595 | 596 | 597 | 598 | 599 | 606 | 617 | 618 | 619 | 620 | 621 | 628 | 639 | 640 | 641 |
604 | {t('common:current-time')}: 605 | 611 | {currDate.toLocaleString( 612 | view24HrTime 613 | ? DateTime.TIME_24_WITH_SHORT_OFFSET 614 | : DateTime.TIME_WITH_SHORT_OFFSET 615 | )} 616 |
626 | {t('common:server-time')}: 627 | 633 | {serverTime.toLocaleString( 634 | view24HrTime 635 | ? DateTime.TIME_24_WITH_SHORT_OFFSET 636 | : DateTime.TIME_WITH_SHORT_OFFSET 637 | )} 638 |
642 | 643 |
644 |
645 |
646 | 647 |
648 | {alertSound !== 'muted' && !unlockedAudio && ( 649 |
{ 652 | setUnlockedAudio(true) 653 | let s = new Howl({ 654 | src: sounds[alertSound as AlertSoundKeys] as unknown as string, 655 | }) 656 | s.play() 657 | }} 658 | > 659 |
660 | 666 | 672 | 673 | Click on me to start receiving alert sounds! 674 |
675 |
676 | )} 677 |
678 | 679 | 680 | 681 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 768 | 780 | 781 | 782 |
685 | {selectedDate.hasSame(serverTime, 'day') 686 | ? 'Alarms' 687 | : `Viewing events ${selectedDate.toRelative()}`} 688 | 689 | {` (Alerts ${alertSound === 'muted' ? 'muted' : 'on'})`} 690 | 714 |
723 | 724 | 725 | 726 | 727 | 741 | 742 | {eventTypeIconMapping.map((e: APIEventType, idx) => ( 743 | 744 | 763 | 764 | ))} 765 | 766 |
728 | 740 |
745 | 762 |
767 |
769 | {currentEventsTable.length > 0 ? ( 770 | 771 | 772 | {currentEventsTable} 773 | 774 |
775 | ) : null} 776 | 777 | {fullEventsTable} 778 |
779 |
783 |
784 |
785 | 786 | ) 787 | } 788 | 789 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 790 | 791 | export async function getStaticProps({ locale }: { locale: string }) { 792 | return { 793 | props: { 794 | ...(await serverSideTranslations(locale, [ 795 | 'events', 796 | 'alarms', 797 | 'common', 798 | 'alarmConfig', 799 | ])), 800 | // Will be passed to the page component as props 801 | }, 802 | } 803 | } 804 | export default Alarms 805 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/items.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | import Item from '../../common/Item' 4 | 5 | type Data = { 6 | items: Item[] 7 | } 8 | 9 | export default function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | res.status(200).json({ items: [new Item('test'), new Item('test2')] }) 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/regions/[region].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { DateTime, Interval } from 'luxon' 3 | import type { NextApiRequest, NextApiResponse } from 'next' 4 | import Region from '../../../common/Region' 5 | import Server from '../../../common/Server' 6 | 7 | type Data = { 8 | servers: Server[] 9 | } 10 | const merchantData = require('../../../data/merchants.json') 11 | const merchantSchedules = require('../../../data/merchantSchedules.json') 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const regions: { 17 | [key: string]: Server[] 18 | } = require('../../../data/regions.json') 19 | 20 | const { region } = req.query 21 | let baseURL = process.env.NEXT_PUBLIC_VERCEL_ENV 22 | ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` 23 | : 'http://localhost:3000' 24 | 25 | const servers = regions[region as string] 26 | if (servers) { 27 | res.status(200).json({ 28 | servers: servers, 29 | }) 30 | } 31 | res.status(400).end('400 bad request') 32 | } 33 | -------------------------------------------------------------------------------- /pages/api/regions/all.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | import Region from '../../../common/Region' 4 | 5 | type Data = { 6 | regions: Region[] 7 | } 8 | 9 | export default function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | const regions = require('../../../data/regions.json') 14 | res.status(200).json({ regions: regions }) 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/server-maintenance.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | isDown: boolean 6 | endTime: string 7 | } 8 | 9 | export default function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | let tz = 'America/Los_Angeles' 14 | let startTime = DateTime.fromObject( 15 | { 16 | month: 3, 17 | day: 17, 18 | hour: 0, 19 | minute: 0, 20 | second: 0, 21 | }, 22 | { zone: tz } 23 | ) 24 | 25 | let endTime = startTime.plus({ hours: 4 }) 26 | res.status(200).json({ 27 | isDown: false, 28 | // startTime.diffNow().toMillis() < 0 && endTime.diffNow().toMillis() > 0, 29 | endTime: endTime.toMillis().toString(), 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import React from 'react' 3 | 4 | import 'core-js/features/array/at' 5 | type HomeProps = { 6 | isDown: boolean 7 | endTime: string 8 | } 9 | const Home: NextPage = (props) => { 10 | return <> 11 | } 12 | export default Home 13 | -------------------------------------------------------------------------------- /pages/merchants.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import React, { useState, useRef, useEffect } from 'react' 4 | import merchantSchedules from '../data/merchantSchedules.json' 5 | import saintbotImage from '../public/images/saint-bot.png' 6 | import { DateTime, Interval } from 'luxon' 7 | import useLocalStorage from '@olerichter00/use-localstorage' 8 | import regions from '../data/regions.json' 9 | import merchantsData from '../data/merchants.json' 10 | import { createTableData } from '../util/createTableData' 11 | import WanderingMerchant from '../common/WanderingMerchant' 12 | import io, { Socket } from 'socket.io-client' 13 | import { MerchantAPIData, RegionKey, ServerKey } from '../util/types/types' 14 | import Image from 'next/image' 15 | import { RegionTimeZoneMapping } from '../util/static' 16 | import { IconSettings } from '@tabler/icons' 17 | import { MerchantConfigModal } from '../components' 18 | 19 | interface Merchant { 20 | location: string 21 | item: string 22 | name: string 23 | } 24 | 25 | const Merchants: NextPage = (props) => { 26 | const { t } = useTranslation('merchants') 27 | const [regionTZName, setRegionTZName] = useLocalStorage( 28 | 'regionTZName', 29 | 'US West' 30 | ) 31 | const [selectedServer, setSelectedServer] = useLocalStorage( 32 | 'merchantServer', 33 | 'Shandi' 34 | ) 35 | const isMounted = useRef(false); 36 | const defaultTheme = () => { 37 | // Defaults to system theme if unconfigured 38 | return (localStorage.getItem('darkMode') || window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) 39 | } 40 | const [darkMode, setDarkMode] = useLocalStorage('darkMode', defaultTheme) 41 | useEffect(()=> { 42 | //Prevents FoUC (Flash of Unstylized Content) by not refreshing on first mount 43 | if (!isMounted.current){ isMounted.current = true; return } 44 | 45 | //Toggle Daisy UI colors (e.g. bg-base-###) 46 | document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light') 47 | 48 | //Toggle standard Tailwind colors (e.g. bg-sky-800) 49 | darkMode 50 | ? document.documentElement.classList.add("dark") 51 | : document.documentElement.classList.remove("dark") 52 | }, [darkMode]) 53 | 54 | const [currDate, setCurrDate] = useState(DateTime.now()) 55 | const [regionTZ, setRegionTZ] = useLocalStorage('regionTZ', 'UTC-7') 56 | 57 | const [serverTime, setServerTime] = useState( 58 | currDate.setZone(regionTZ) 59 | ) 60 | const [viewLocalizedTime, setViewLocalizedTime] = useLocalStorage( 61 | 'viewLocalizedTime', 62 | true 63 | ) 64 | const [view24HrTime, setView24HrTime] = useLocalStorage( 65 | 'view24HrTime', 66 | false 67 | ) 68 | const [mSchedules, setMSchedules] = useState<{ [k: string]: Interval[] }>({}) 69 | 70 | useEffect(() => { 71 | const timer = setInterval(() => { 72 | let now = DateTime.now() 73 | setCurrDate(now) 74 | setServerTime(now.setZone(regionTZ)) 75 | }, 1000) 76 | return () => { 77 | clearInterval(timer) // Return a funtion to clear the timer so that it will stop being called on unmount 78 | } 79 | }, [regionTZName, regionTZ]) 80 | 81 | useEffect(() => { 82 | if (regionTZ) setServerTime(DateTime.now().setZone(regionTZ)) 83 | }, [regionTZName, regionTZ]) 84 | const [merchantAPIData, setMerchantAPIData] = useState<{ 85 | [key: string]: MerchantAPIData 86 | }>({}) 87 | const [socket, setSocket] = useState(null) 88 | const [merchantTableData, setMerchantTableData] = useState< 89 | Array 90 | >([]) 91 | 92 | const [wanderingMerchants, setWanderingMerchants] = useState< 93 | WanderingMerchant[] 94 | >([]) 95 | const [apiData, setAPIData] = useState<{ [key: string]: MerchantAPIData }>({}) 96 | const [dataLastRefreshed, setDataLastRefreshed] = useState(currDate) 97 | useEffect(() => { 98 | const newSocket = io(`wss://ws.lostarktimer.app`) 99 | 100 | if (process.env.NEXT_PUBLIC_VERCEL_ENV !== 'production') { 101 | newSocket.disconnect() 102 | } 103 | 104 | newSocket.on('merchants', (data) => { 105 | setAPIData(data) 106 | }) 107 | 108 | setSocket(newSocket) 109 | return () => { 110 | newSocket.close() 111 | } 112 | }, []) 113 | 114 | useEffect(() => { 115 | setMerchantAPIData({ ...merchantAPIData, ...apiData }) 116 | setDataLastRefreshed(DateTime.now()) 117 | }, [apiData]) 118 | 119 | useEffect(() => { 120 | if (regionTZName) { 121 | let servers = regions[regionTZName] 122 | if (!servers.includes(selectedServer || '')) 123 | setSelectedServer(servers[0] as ServerKey) 124 | let newMSchedules: { [key: string]: Interval[] } = {} 125 | Object.entries(merchantSchedules).forEach(([key, val]) => { 126 | newMSchedules[key] = val.map(({ h, m }) => { 127 | let start = DateTime.fromObject( 128 | { hour: h, minute: m }, 129 | { zone: regionTZ } 130 | ) 131 | return Interval.fromDateTimes(start, start.plus({ minutes: 25 })) 132 | }) 133 | let dayPlusOne = DateTime.fromObject( 134 | { hour: val[0].h, minute: val[0].m }, 135 | { zone: regionTZ } 136 | ).plus({ days: 1 }) 137 | newMSchedules[key].push( 138 | Interval.fromDateTimes(dayPlusOne, dayPlusOne.plus({ minutes: 25 })) 139 | ) 140 | }) 141 | setMSchedules(newMSchedules) 142 | setWanderingMerchants( 143 | Object.values(merchantsData).map( 144 | (m) => 145 | new WanderingMerchant( 146 | m.name, 147 | m.items, 148 | m.continent, 149 | m.schedule, 150 | m.locations as {}, 151 | newMSchedules[String(m.schedule)] 152 | ) 153 | ) 154 | ) 155 | } 156 | }, [regionTZName]) 157 | useEffect(() => { 158 | if (socket?.connected && selectedServer) { 159 | socket.removeAllListeners() 160 | socket.emit('join', `${selectedServer.toLowerCase()}`) 161 | socket.on('merchants', (data) => { 162 | setAPIData(data) 163 | }) 164 | } 165 | }, [socket?.connected, selectedServer]) 166 | useEffect(() => { 167 | let data = Object.values(merchantAPIData).filter( 168 | (m) => m.server === selectedServer?.toLowerCase() 169 | ) 170 | wanderingMerchants.forEach((wm) => { 171 | let fm = data.find((m) => wm.name === m.name) 172 | if (fm) wm.setSpawn(fm.location, fm.item, Number(fm._id)) 173 | else wm.unsetSpawn() 174 | }) 175 | 176 | wanderingMerchants.sort((a, b) => { 177 | let inProgA = a.inProgress(serverTime) 178 | let inProgB = b.inProgress(serverTime) 179 | 180 | if (inProgA && inProgB) return a.name.localeCompare(b.name) 181 | else if (inProgA) return -1 182 | else if (inProgB) return 1 183 | // return a.name.localeCompare(b.name) 184 | let aSpawn = a.nextSpawnTime(serverTime)?.start 185 | let bSpawn = b.nextSpawnTime(serverTime)?.start 186 | if (aSpawn && bSpawn) { 187 | if (aSpawn.hour == bSpawn.hour) return a.name.localeCompare(b.name) 188 | else return aSpawn.diff(bSpawn).toMillis() 189 | } 190 | return a.name.localeCompare(b.name) 191 | }) 192 | 193 | setMerchantTableData( 194 | createTableData({ 195 | events: wanderingMerchants, 196 | serverTime: serverTime, 197 | currDate: currDate, 198 | viewLocalizedTime: viewLocalizedTime || false, 199 | view24HrTime: view24HrTime || false, 200 | isGameEvent: false, 201 | }) 202 | ) 203 | }, [ 204 | regionTZName, 205 | view24HrTime, 206 | viewLocalizedTime, 207 | selectedServer, 208 | wanderingMerchants, 209 | merchantAPIData, 210 | mSchedules, 211 | ]) 212 | useEffect(() => { 213 | if (currDate.minute < 30 || currDate.minute >= 55) setMerchantAPIData({}) 214 | }, [currDate.minute]) 215 | return ( 216 | <> 217 | 218 | Merchants - Lost Ark Timer 219 | 220 | 221 |
222 |
223 |
224 | 231 |
232 | 238 | 239 |
240 | 258 | {regionTZName && ( 259 | 270 | )} 271 |
272 | 273 | 274 | 279 | 280 | 287 | 288 | 289 | 294 | 295 | 302 | 303 | 304 |
{t('common:current-time')}: 281 | {currDate.toLocaleString( 282 | view24HrTime 283 | ? DateTime.TIME_24_WITH_SHORT_OFFSET 284 | : DateTime.TIME_WITH_SHORT_OFFSET 285 | )} 286 |
{t('common:server-time')}: 296 | {serverTime.toLocaleString( 297 | view24HrTime 298 | ? DateTime.TIME_24_WITH_SHORT_OFFSET 299 | : DateTime.TIME_WITH_SHORT_OFFSET 300 | )} 301 |
305 |
306 | 307 |
308 | 309 | 310 | 311 | 344 | 345 | 346 | 347 | 348 | 474 | 475 | 476 | 484 | 485 | 486 |
312 | Wandering Merchants 313 | 319 | {t('vote')} 320 | 321 | 327 | {t('data-by')} SaintBot{' '} 328 | 334 | 335 |
336 | {t('last-updated')}:{' '} 337 | {dataLastRefreshed.toLocaleString( 338 | view24HrTime 339 | ? DateTime.TIME_24_WITH_SECONDS 340 | : DateTime.TIME_WITH_SECONDS 341 | )} 342 |
343 |
352 |
353 | Schedule 1 354 |
355 |
356 | 357 |
    358 | {mSchedules[1] !== undefined && 359 | mSchedules[1] 360 | .slice(0, (mSchedules[1].length - 1) / 2) 361 | .map((s) => 362 | s.start.setZone( 363 | viewLocalizedTime 364 | ? currDate.zone 365 | : serverTime.zone 366 | ) 367 | ) 368 | .sort((a, b) => a.hour - b.hour) 369 | .map((schedule) => ( 370 |
  • 374 | {schedule 375 | .toLocaleString(DateTime.TIME_SIMPLE) 376 | .slice(0, -2)} 377 |
  • 378 | ))} 379 |
380 |
381 |
382 | 383 |
    384 |
  • Lucas - {t('locations.Yudia')}
  • 385 |
  • Morris - {t('locations.East Luterra')}
  • 386 |
  • Mac - {t('locations.Anikka')}
  • 387 |
  • Jeffrey - {t('locations.Shushire')}
  • 388 |
  • Dorella - {t('locations.Feiton')}
  • 389 |
390 |
391 |
392 |
393 |
394 | Schedule 2 395 |
396 |
397 | 398 |
    399 | {mSchedules[2] !== undefined && 400 | mSchedules[2] 401 | .slice(0, (mSchedules[2].length - 1) / 2) 402 | .map((s) => 403 | s.start.setZone( 404 | viewLocalizedTime 405 | ? currDate.zone 406 | : serverTime.zone 407 | ) 408 | ) 409 | .sort((a, b) => a.hour - b.hour) 410 | .map((schedule) => ( 411 |
  • 415 | {schedule 416 | .toLocaleString(DateTime.TIME_SIMPLE) 417 | .slice(0, -2)} 418 |
  • 419 | ))} 420 |
421 |
422 | 423 |
    424 |
  • Malone - {t('locations.West Luterra')}
  • 425 |
  • Burt - {t('locations.East Luterra')}
  • 426 |
  • Oliver - {t('locations.Tortoyk')}
  • 427 |
  • Nox - {t('locations.Arthetine')}
  • 428 |
  • Aricer - {t('locations.Rohendel')}
  • 429 |
  • Rayni - {t('locations.Punika')}
  • 430 |
431 |
432 |
433 |
434 |
435 | Schedule 3 436 |
437 |
438 | 439 |
    440 | {mSchedules[3] !== undefined && 441 | mSchedules[3] 442 | .slice(0, (mSchedules[3].length - 1) / 2) 443 | .map((s) => 444 | s.start.setZone( 445 | viewLocalizedTime 446 | ? currDate.zone 447 | : serverTime.zone 448 | ) 449 | ) 450 | .sort((a, b) => a.hour - b.hour) 451 | .map((schedule) => ( 452 |
  • 456 | {schedule 457 | .toLocaleString(DateTime.TIME_SIMPLE) 458 | .slice(0, -2)} 459 |
  • 460 | ))} 461 |
462 |
463 | 464 |
    465 |
  • Ben - {t('locations.Rethramis')}
  • 466 |
  • Evan - {t('locations.South Vern')}
  • 467 |
  • Peter - {t('locations.North Vern')}
  • 468 |
  • Laitir - {t('locations.Yorn')}
  • 469 |
470 |
471 |
472 |
473 |
477 | 478 | 479 | 480 | 481 | {merchantTableData} 482 |
483 |
487 |
488 |
489 | 490 | ) 491 | } 492 | 493 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 494 | import { useTranslation } from 'next-i18next' 495 | import classNames from 'classnames' 496 | 497 | export async function getStaticProps({ locale }: { locale: string }) { 498 | return { 499 | props: { 500 | ...(await serverSideTranslations(locale, [ 501 | 'merchants', 502 | 'common', 503 | 'merchantConfig', 504 | ])), 505 | // Will be passed to the page component as props 506 | }, 507 | } 508 | } 509 | 510 | export default Merchants 511 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/favicon.ico -------------------------------------------------------------------------------- /public/images/LA_Mokko_Seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/LA_Mokko_Seed.png -------------------------------------------------------------------------------- /public/images/language_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/language_icon.png -------------------------------------------------------------------------------- /public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_BREEZESOME_BRAE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_BREEZESOME_BRAE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_ELZOWINS_SHADE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_ELZOWINS_SHADE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_GLASS_LOTUS_LAKE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_GLASS_LOTUS_LAKE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_LAKE_SHIVERWAVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_LAKE_SHIVERWAVE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_XENEELA_RUINS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/ARICER_ROHENDEL/ARICER_ROHENDEL_XENEELA_RUINS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_ANKUMO_MOUNTAIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_ANKUMO_MOUNTAIN.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_LOGHILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_LOGHILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_RETHRAMIS_BORDER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BEN_RETHRAMIS/BEN_RETHRAMIS_RETHRAMIS_BORDER.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_BLACKROSE_CHAPEL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_BLACKROSE_CHAPEL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_BOREAS_DOMAIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_BOREAS_DOMAIN.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE_NORTH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE_NORTH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE_SOUTH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_CROCONYS_SEASHORE_SOUTH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_LEYAR_TERRACE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/BURT_EAST_LUTERRA/BURT_EAST_LUTERRA_LEYAR_TERRACE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/DORELLA_FEITON/DORELLA_FEITON_KALAJA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/DORELLA_FEITON/DORELLA_FEITON_KALAJA.png -------------------------------------------------------------------------------- /public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_BITTERWIND_HILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_BITTERWIND_HILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_FROZEN_SEA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_FROZEN_SEA.png -------------------------------------------------------------------------------- /public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_ICEBLOOD_PLATEAU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_ICEBLOOD_PLATEAU.png -------------------------------------------------------------------------------- /public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_ICEWING_HEIGHTS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_ICEWING_HEIGHTS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_LAKE_ETERNITY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/JEFFREY_SHUSHIRE/JEFFREY_SHUSHIRE_LAKE_ETERNITY.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_BLACK_ANVIL_MINE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_BLACK_ANVIL_MINE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_HALL_OF_PROMISE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_HALL_OF_PROMISE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_IRON_HAMMER_MINE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_IRON_HAMMER_MINE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_UNFINISHED_GARDEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_UNFINISHED_GARDEN.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_YORNS_CRADLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LAITIR_YORN/LAITIR_YORN_YORNS_CRADLE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LUCAS_YUDIA/LUCAS_YUDIA_OZHORN_HILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LUCAS_YUDIA/LUCAS_YUDIA_OZHORN_HILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/LUCAS_YUDIA/LUCAS_YUDIA_SALAND_HILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/LUCAS_YUDIA/LUCAS_YUDIA_SALAND_HILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_DELPHI_TOWNSHIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_DELPHI_TOWNSHIP.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_MELODY_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_MELODY_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_PRISMA_VALLEY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_PRISMA_VALLEY.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_RATTAN_HILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_RATTAN_HILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_TWILIGHT_MISTS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MAC_ANIKKA/MAC_ANIKKA_TWILIGHT_MISTS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_BATTLEBOUND_PLAINS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_BATTLEBOUND_PLAINS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_BILBRIN_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_BILBRIN_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_LAKEBAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_LAKEBAR.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_MEDRICK_MONASTERY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_MEDRICK_MONASTERY.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_MOUNT_ZAGORAS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MALONE_WEST_LUTERRA/MALONE_WEST_LUTERRA_MOUNT_ZAGORAS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_DYORIKA_PLAIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_DYORIKA_PLAIN.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_FLOWERING_ORCHARD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_FLOWERING_ORCHARD.png -------------------------------------------------------------------------------- /public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_SUNBRIGHT_HILL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/MORRIS_EAST_LUTERRA/MORRIS_EAST_LUTERRA_SUNBRIGHT_HILL.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_ARID_PATH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_ARID_PATH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_NEBELHORN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_NEBELHORN.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_RIZA_FALLS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_RIZA_FALLS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_SCRAPLANDS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_SCRAPLANDS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_TOTRICH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_TOTRICH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_WINDBRINGER_HILLS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/NOX_ARTHETINE/NOX_ARTHETINE_WINDBRINGER_HILLS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_FOREST_OF_GIANTS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_FOREST_OF_GIANTS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SEASWEPT_WOODS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SEASWEPT_WOODS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SKYREACH_STEPPE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SKYREACH_STEPPE.png -------------------------------------------------------------------------------- /public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SWEETWATER_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/OLIVER_TORTOYK/OLIVER_TORTOYK_SWEETWATER_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_BALANKAR_MOUNTAINS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_BALANKAR_MOUNTAINS.png -------------------------------------------------------------------------------- /public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_FESNAR_HIGHLAND.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_FESNAR_HIGHLAND.png -------------------------------------------------------------------------------- /public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_PARNA_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_PARNA_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_PORT_KRONA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_PORT_KRONA.png -------------------------------------------------------------------------------- /public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_VERNESE_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/PETER_NORTH_VERN/PETER_NORTH_VERN_VERNESE_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_SECRET_FOREST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_SECRET_FOREST.png -------------------------------------------------------------------------------- /public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_STARSAND_BEACH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_STARSAND_BEACH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_TIDESHELF_PATH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_TIDESHELF_PATH.png -------------------------------------------------------------------------------- /public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_TIKATIKA_COLONY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/merchantLocations/RAYNI_PUNIKA/RAYNI_PUNIKA_TIKATIKA_COLONY.png -------------------------------------------------------------------------------- /public/images/saint-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/images/saint-bot.png -------------------------------------------------------------------------------- /public/locales/en/alarmConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "alarm-settings": "Alarm Settings", 3 | "move-disabled-events-to-bottom": "Move Disabled Events to Bottom", 4 | "hide-disabled-events": "Hide Disabled Events", 5 | "group-repeat-events": "Group Repeat Events", 6 | "reset-disabled-events": "Reset Disabled Events", 7 | "alert-sound": "Alert Sound", 8 | "muted": "Muted", 9 | "enable-desktop-notifications": "Enable Desktop Notifications" 10 | } 11 | -------------------------------------------------------------------------------- /public/locales/en/alarms.json: -------------------------------------------------------------------------------- 1 | { 2 | "notification": { 3 | "heading": "Events Starting in {{notifyInMins}} minutes", 4 | "additional-events": "+{{additionalEvents}} more" 5 | }, 6 | "repeated-events": { 7 | "show": "Show Repeated Events", 8 | "hide": "Hide Repeated Events" 9 | }, 10 | "enable": "Enable Alarm", 11 | "disable": { 12 | "once": "Disable Once", 13 | "12hrs": "Disable Alarm for 12 Hours", 14 | "daily-reset": "Disable Until Daily Reset", 15 | "weekly-reset": "Disable Until Weekly Reset", 16 | "all": "Disable All Future Alarms" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "alarm-link-text": "Alarms", 3 | "merchant-link-text": "Merchants", 4 | "current-time": "Current Time", 5 | "server-time": "Server Time", 6 | "view-in-24-hr": "View in 24HR", 7 | "option-minute-before": "{{minutes}} min before", 8 | "view-in-current-time": "View in Current Time", 9 | "dark-mode": "Dark Mode", 10 | "all-done": "All Done" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/en/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": { 3 | "Fever Time": "Fever Time", 4 | "Capture Event": "Capture Event", 5 | "Chaos Gate": "Chaos Gate", 6 | "Field Boss": "Field Boss", 7 | "Adventure Island": "Adventure Island", 8 | "Ghost Ship": "Ghost Ship", 9 | "Islands": "Islands", 10 | "Sailing": "Sailing", 11 | "Siege Content": "Siege Content", 12 | "Rowen": "Rowen", 13 | "Proving Grounds": "Proving Grounds", 14 | "Siege": "Siege" 15 | }, 16 | "4005": "Proving Grounds Co-op Battle", 17 | "4001": "Proving Grounds Competitive Match", 18 | "4003": "Proving Grounds Deathmatch", 19 | "4002": "Proving Grounds Team Deathmatch", 20 | "4004": "Proving Grounds Team Elimination Match", 21 | "7046": "Asura Island", 22 | "7047": "Drumbeat Island", 23 | "7038": "Forpe", 24 | "7042": "Harmony Island", 25 | "7043": "Lagoon Island", 26 | "7049": "Lush Reed Island", 27 | "7040": "Medeia", 28 | "7041": "Monte Island", 29 | "7039": "Oblivion Isle", 30 | "7044": "Opportunity Isle", 31 | "7050": "Phantomwing Island", 32 | "7048": "Snowpang Island", 33 | "7045": "Tranquil Isle", 34 | "7037": "Volare Island", 35 | "935": "Waterpop Arena", 36 | "938": "Blooming Mokokos!", 37 | "950": "Arkesia Grand Prix (Great Castle)", 38 | "948": "Arkesia Grand Prix (Kalaja)", 39 | "947": "Arkesia Grand Prix (Luterra Castle)", 40 | "951": "Arkesia Grand Prix (Nia Village)", 41 | "945": "Arkesia Grand Prix (Rothun)", 42 | "946": "Arkesia Grand Prix (Stern)", 43 | "949": "Arkesia Grand Prix (Vern Castle)", 44 | "7033": "Alakkir", 45 | "7005": "Death's Hold Island", 46 | "7016": "Illusion Isle", 47 | "7013": "Lullaby Island", 48 | "7018": "Spida Island", 49 | "7014": "Tooki Island", 50 | "6007": "Gate of Harmony", 51 | "6001": "Sailing Co-op: Anikka", 52 | "6000": "Sailing Co-op: Arthetine", 53 | "6002": "Sailing Co-op: Vern", 54 | "1002": "Twisting Chaos Legion", 55 | "1003": "Twisting Darkness Legion", 56 | "1001": "Twisting Phantom Legion", 57 | "1004": "Twisting Plague Legion", 58 | "3008": "Chaotic Chuo", 59 | "3005": "Signatus", 60 | "3003": "Tarsila", 61 | "3011": "Erasmo", 62 | "6008": "Gate of Wisdom", 63 | "5002": "Nightmare Ghost Ship", 64 | "6003": "Sailing Co-op: Rohendel", 65 | "7019": "Shangra", 66 | "1009": "Twisting Phantom Legion", 67 | "7020": "Unknown Island", 68 | "3013": "Magmadon", 69 | "3001": "Proxima", 70 | "7035": "Gesbroy", 71 | "6009": "Gate of Earth", 72 | "6004": "Sailing Co-op: Yorn", 73 | "1007": "Twisting Plague Legion", 74 | "3006": "Harvest Lord Incarnate", 75 | "3012": "Kohinorr", 76 | "6010": "Gate of Endurance", 77 | "6005": "Sailing Co-op: Feiton", 78 | "5003": "Shadow Ghost Ship", 79 | "1006": "Twisting Darkness Legion", 80 | "3014": "Ancheladus", 81 | "3002": "Sol Grande", 82 | "6011": "Gate of Guidance", 83 | "6006": "Sailing Co-op: Punika", 84 | "1008": "Twisting Chaos Legion", 85 | "5004": "Tempest Ghost Ship", 86 | "3007": "Aurion", 87 | "3004": "Brealeos", 88 | "3015": "Moake", 89 | "3016": "Thunderwings", 90 | "8000": "Medeia Capture Event", 91 | "8001": "Slime Island Capture Event", 92 | "2003": "Siege Registration Period", 93 | "2002": "Raid Match Registration Period", 94 | "1010": "Twisting Demon Legion", 95 | "5005": "Sinful Ghost Ship", 96 | "916": "[Event] Festivity Island", 97 | "8006": "Mercenary Join Request", 98 | "9002": "[Assail]Liebertane-[Siege]-[Raid]Preigelli", 99 | "9001": "[Assail]Preigelli-[Siege]-[Raid]Liebertane", 100 | "3022": "Hermut (Liebertane)", 101 | "3021": "Hermut (Preigelli)", 102 | "5006": "Phantom Ghost Ship", 103 | "964": "Steaming Hot Spring (Great Castle)", 104 | "962": "Steaming Hot Spring (Kalaja)", 105 | "961": "Steaming Hot Spring (Luterra Castle)", 106 | "965": "Steaming Hot Spring (Nia Village)", 107 | "959": "Steaming Hot Spring (Rothun)", 108 | "960": "Steaming Hot Spring (Stern)", 109 | "963": "Steaming Hot Spring (Vern Castle)", 110 | "9050": "[Battlefield] Tulubik Battlegrounds", 111 | "8007": "Siege PvP Base Entry" 112 | } 113 | -------------------------------------------------------------------------------- /public/locales/en/merchantConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "merchant-settings": "Merchant Settings", 3 | "hide-merchant-items": "Hide Merchant Items", 4 | "hide-merchant-potential-spawns": "Hide Merchant Potential Spawns" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/en/merchants.json: -------------------------------------------------------------------------------- 1 | { 2 | "next-spawn": "Next Spawn", 3 | "potential-spawns": "Potential Spawns", 4 | "location": "Location", 5 | "item": "Item", 6 | "rapport": "Rapport", 7 | "card": "Card", 8 | "cooking": "Cooking", 9 | "vote": "Vote", 10 | "server-note-text": "NOTE: TIMES SHOWN CURRENTLY ARE IN {{timeType}}.", 11 | 12 | "locations": { 13 | "Rethramis": "Rethramis", 14 | "Ankumo Mountain": "Ankumo Mountain", 15 | "Log Hill": "Log Hill", 16 | "Rethramis Border": "Rethramis Border", 17 | "Yudia": "Yudia", 18 | "Ozhorn Hill": "Ozhorn Hill", 19 | "Saland Hill": "Saland Hill", 20 | "West Luterra": "West Luterra", 21 | "Battlebound Plains": "Battlebound Plains", 22 | "Bilbrin Forest": "Bilbrin Forest", 23 | "Lakebar": "Lakebar", 24 | "Medrick Monastery": "Medrick Monastery", 25 | "Mount Zagoras": "Mount Zagoras", 26 | "East Luterra": "East Luterra", 27 | "Dyorika Plain": "Dyorika Plain", 28 | "Flowering Orchard": "Flowering Orchard", 29 | "Sunbright Hill": "Sunbright Hill", 30 | "Blackrose Chapel": "Blackrose Chapel", 31 | "Boreas Domain": "Boreas Domain", 32 | "Croconys Seashore North": "Croconys Seashore North", 33 | "Croconys Seashore South": "Croconys Seashore South", 34 | "Leyar Terrace": "Leyar Terrace", 35 | "Tortoyk": "Tortoyk", 36 | "Forest of Giants": "Forest of Giants", 37 | "Seaswept Woods": "Seaswept Woods", 38 | "Skyreach Steppe": "Skyreach Steppe", 39 | "Sweetwater Forest": "Sweetwater Forest", 40 | "Anikka": "Anikka", 41 | "Delphi Township": "Delphi Township", 42 | "Melody Forest": "Melody Forest", 43 | "Prisma Valley": "Prisma Valley", 44 | "Rattan Hill": "Rattan Hill", 45 | "Twilight Mists": "Twilight Mists", 46 | "Arthetine": "Arthetine", 47 | "Arid Path": "Arid Path", 48 | "Nebelhorn": "Nebelhorn", 49 | "Riza Falls": "Riza Falls", 50 | "Scraplands": "Scraplands", 51 | "Totrich": "Totrich", 52 | "Windbringer Hill": "Windbringer Hill", 53 | "North Vern": "North Vern", 54 | "Balankar Mountains": "Balankar Mountains", 55 | "Fesnar Highland": "Fesnar Highland", 56 | "Parna Forest": "Parna Forest", 57 | "Port Krona": "Port Krona", 58 | "Vernese Forest": "Vernese Forest", 59 | "Shushire": "Shushire", 60 | "Bitterwind Hill": "Bitterwind Hill", 61 | "Frozen Sea": "Frozen Sea", 62 | "Iceblood Plateau": "Iceblood Plateau", 63 | "Icewing Heights": "Icewing Heights", 64 | "Lake Eternity": "Lake Eternity", 65 | "Rohendel": "Rohendel", 66 | "Breezesome Brae": "Breezesome Brae", 67 | "Elzowins Shade": "Elzowins Shade", 68 | "Glass Lotus Lake": "Glass Lotus Lake", 69 | "Lake Shiverwave": "Lake Shiverwave", 70 | "Xeneela Ruins": "Xeneela Ruins", 71 | "Yorn": "Yorn", 72 | "Black Anvil Mine": "Black Anvil Mine", 73 | "Hall of Promise": "Hall of Promise", 74 | "Iron Hammer Mine": "Iron Hammer Mine", 75 | "Unfinished Garden": "Unfinished Garden", 76 | "Yorn's Cradle": "Yorn's Cradle", 77 | "Feiton": "Feiton", 78 | "Kalaja": "Kalaja", 79 | "Punika": "Punika", 80 | "Tideshelf Path": "Tideshelf Path", 81 | "Starsand Beach": "Starsand Beach", 82 | "Tikatika Colony": "Tikatika Colony", 83 | "Secret Forest": "Secret Forest", 84 | "South Vern": "South Vern", 85 | "Candaria Territory": "Candaria Territory", 86 | "Bellion Ruins": "Bellion Ruins" 87 | }, 88 | "items": { 89 | "920404": { 90 | "name": "Adrenaline-boosting Fluid" 91 | }, 92 | "920701": { "name": "Back Alley Rum" }, 93 | "920803": { 94 | "name": "Raw Boar Meat" 95 | }, 96 | "921004": { "name": "Blood Pudding Chunk" }, 97 | "921405": { "name": "Hairplant" }, 98 | "930605": { "name": "Sapphire Sardine" }, 99 | "930902": { "name": "Pit-A-Pat Macaron" }, 100 | "931505": { "name": "Dry-aged Meat" }, 101 | "931512": { 102 | "name": "Hot Chocolate Coffee" 103 | }, 104 | "63200008": { "name": "Wei" }, 105 | "63200024": { "name": "Thunder Wings" }, 106 | "63200030": { "name": "Mokamoka" }, 107 | "63200035": { "name": "Krause" }, 108 | "63200038": { "name": "Madnick" }, 109 | "63200039": { "name": "Thar" }, 110 | "63200050": { "name": "Gnosis" }, 111 | "63200053": { "name": "Kaysarr" }, 112 | "63200057": { "name": "Kaldor" }, 113 | "63200094": { "name": "Varut" }, 114 | "63200095": { "name": "Prideholme Neria" }, 115 | "63200102": { "name": "Thunder" }, 116 | "63200104": { "name": "Meehan" }, 117 | "63200106": { "name": "Cassleford" }, 118 | "63200108": { "name": "Alifer" }, 119 | "63200111": { "name": "Seria" }, 120 | "63200118": { "name": "Nox" }, 121 | "63200122": { "name": "Eolh" }, 122 | "63200136": { "name": "Stern Neria" }, 123 | "63200144": { "name": "Sian" }, 124 | "63200146": { "name": "Gideon" }, 125 | "63200148": { "name": "Payla" }, 126 | "63200158": { "name": "Lenora" }, 127 | "63200164": { 128 | "name": "Great Castle Neria" 129 | }, 130 | "63200165": { "name": "Piyer" }, 131 | "63200170": { "name": "Levi" }, 132 | "63200171": { "name": "Goulding" }, 133 | "63200174": { "name": "Bergstrom" }, 134 | "63200185": { "name": "Giant Worm" }, 135 | "63200188": { "name": "Cadogan" }, 136 | "63200191": { "name": "Berhart" }, 137 | "63200192": { "name": "Brinewt" }, 138 | "63200197": { "name": "Egg of Creation" }, 139 | "63200205": { "name": "Sir Druden" }, 140 | "63200206": { "name": "Sir Valleylead" }, 141 | "63200209": { "name": "Javern" }, 142 | "63200222": { "name": "Siera" }, 143 | "63200226": { "name": "Morpheo" }, 144 | "63200227": { "name": "Morina" }, 145 | "63200228": { "name": "Madam Moonscent" }, 146 | "63200305": { "name": "Albion" }, 147 | "63200308": { "name": "Seto" }, 148 | "63200309": { "name": "Cicerra" }, 149 | "63200310": { "name": "Stella" }, 150 | "70500000": { "name": "Fancier Boquet" }, 151 | "70500001": { 152 | "name": "Prideholme Potato" 153 | }, 154 | "70500002": { 155 | "name": "Rethramis Holy Water" 156 | }, 157 | "70500003": { 158 | "name": "Yudia Natural Salt" 159 | }, 160 | "70500006": { "name": "Stalwart Cage" }, 161 | "70500007": { 162 | "name": "Dyorika Straw Hat" 163 | }, 164 | "70500008": { 165 | "name": "Model of Luterra's Sword" 166 | }, 167 | "70500011": { "name": "Lakebar Tomato Juice" }, 168 | "70500012": { 169 | "name": "Azenaporium Brooch" 170 | }, 171 | "70500013": { "name": "Black Rose" }, 172 | "70500014": { "name": "Round Glass Piece" }, 173 | "70500015": { "name": "Mokoko Carrot" }, 174 | "70500016": { 175 | "name": "Oversize Ladybug Doll" 176 | }, 177 | "70500024": { "name": "Magick Cloth" }, 178 | "70500025": { "name": "Magick Crystal" }, 179 | "70500026": { 180 | "name": "Exquisite Music Box" 181 | }, 182 | "70500027": { 183 | "name": "Queen's Knights Application" 184 | }, 185 | "70500028": { "name": "Goblin Yarn" }, 186 | "70500033": { "name": "Soundstone of Dawn" }, 187 | "70500034": { "name": "Elemental's Feather" }, 188 | "70500037": { "name": "Fargar's Beer" }, 189 | "70500038": { "name": "Broken Dagger" }, 190 | "70500039": { "name": "Book of Survival" }, 191 | "70500040": { 192 | "name": "Dessicated Wooden Statue" 193 | }, 194 | "70500056": { "name": "Danube's Earrings" }, 195 | "70500059": { "name": "Hollowfruit" }, 196 | "70500060": { "name": "Piñata Crafting Set" }, 197 | "70500061": { 198 | "name": "Rainbow Tikatika Flower" 199 | }, 200 | "70501000": { "name": "Surprise Chest" }, 201 | "70501001": { 202 | "name": "Sky Reflection Oil" 203 | }, 204 | "70501002": { 205 | "name": "Chain War Chronicles" 206 | }, 207 | "70501003": { 208 | "name": "Shy Wind Flower Pollen" 209 | }, 210 | "70501004": { 211 | "name": "Angler's Fishing Pole" 212 | }, 213 | "70501005": { "name": "Fine Gramophone" }, 214 | "70501006": { 215 | "name": "Vern's Founding Coin" 216 | }, 217 | "70501007": { 218 | "name": "Sirius's Holy Book" 219 | }, 220 | "70501008": { "name": "Red Moon's Tears" }, 221 | "70501010": { 222 | "name": "Sylvain Queen's Blessing" 223 | }, 224 | "70501011": { "name": "Oreha Viewing Stone" }, 225 | "70503000": { 226 | "name": "Tournament Entrance Stamp" 227 | }, 228 | "70503001": { "name": "Yudia Spellbook" }, 229 | "70503005": { "name": "Shimmering Essence" }, 230 | "70503006": { "name": "Energy X7 Capsule" }, 231 | "70503008": { "name": "Piyer's Secret Textbook" }, 232 | 233 | "70500063": { "name": "Feather Fan" }, 234 | "70500062": { "name": "Febre Potion" }, 235 | "70500064": { "name": "Mockup Firefly" }, 236 | "70501012": { "name": "Necromancer's Records" }, 237 | 238 | "63200407": { "name": "Satra" }, 239 | "63200408": { "name": "Killian" }, 240 | "63200402": { "name": "Lujean" }, 241 | "63200404": { "name": "Vern Zenlord" }, 242 | "63200403": { "name": "Xereon" } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /public/locales/zh/alarmConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "alarm-settings": "Alarm Settings", 3 | "move-disabled-events-to-bottom": "禁用事件移动到底部", 4 | "hide-disabled-events": "隐藏禁用的事件", 5 | "group-repeat-events": "隐藏重复的事件 [大奖赛]", 6 | "reset-disabled-events": "重置禁用的事件", 7 | "alert-sound": "提醒声音", 8 | "muted": "静音", 9 | "enable-desktop-notifications": "启用桌面通知" 10 | } 11 | -------------------------------------------------------------------------------- /public/locales/zh/alarms.json: -------------------------------------------------------------------------------- 1 | { 2 | "notification": { 3 | "heading": "Events Starting in {{notifyInMins}} minutes", 4 | "additional-events": "+{{additionalEvents}} more" 5 | }, 6 | "repeated-events": { 7 | "show": "Show Repeated Events", 8 | "hide": "Hide Repeated Events" 9 | }, 10 | "enable": "Enable Alarm", 11 | "disable": { 12 | "once": "Disable Once", 13 | "12hrs": "Disable Alarm for 12 Hours", 14 | "daily-reset": "Disable Until Daily Reset", 15 | "weekly-reset": "Disable Until Weekly Reset", 16 | "all": "Disable All Future Alarms" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/locales/zh/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "alarm-link-text": "活动提醒", 3 | "merchant-link-text": "流浪商人", 4 | "current-time": "当前时间", 5 | "server-time": "服务器时间", 6 | "view-in-24-hr": "查看24小时内的", 7 | "option-minute-before": "提前{{minutes}}分钟提醒", 8 | "view-in-current-time": "当前时间内查看", 9 | "dark-mode": "Dark Mode", 10 | "all-done": "确定" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/zh/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": { 3 | "Fever Time": "热点事件", 4 | "Capture Event": "捕获事件", 5 | "Chaos Gate": "混沌之门", 6 | "Field Boss": "野外BOSS", 7 | "Adventure Island": "冒险岛", 8 | "Ghost Ship": "幽灵船", 9 | "Islands": "岛", 10 | "Sailing": "航海", 11 | "Siege": "围攻", 12 | "Proving Grounds": "勇气战场", 13 | "Siege Content": "Siege Content" 14 | }, 15 | "4005": "勇气战场:合作站", 16 | "4001": "勇气战场:竞赛", 17 | "4003": "勇气战场:死亡竞赛", 18 | "4002": "勇气战场:团队死亡竞赛", 19 | "4004": "勇气战场:淘汰赛", 20 | "7046": "修罗岛", 21 | "7047": "咚咚岛", 22 | "7038": "波尔佩", 23 | "7042": "和声岛", 24 | "7043": "蓝洞岛", 25 | "7049": "芦苇岛", 26 | "7040": "美狄亚", 27 | "7041": "蒙特岛", 28 | "7039": "死亡峡谷", 29 | "7044": "机遇岛", 30 | "7050": "迎蝶岛", 31 | "7048": "雪坊岛", 32 | "7045": "平静的安息岛", 33 | "7037": "伏勒尔岛", 34 | "950": "亚克拉西亚大奖赛(伟大的城堡)", 35 | "948": "亚克拉西亚大奖赛(卡拉扎村)", 36 | "947": "亚克拉西亚大奖赛(卢特兰城)", 37 | "951": "亚克拉西亚大奖赛(尼亚村)", 38 | "945": "亚克拉西亚大奖赛(罗亚伦)", 39 | "946": "亚克拉西亚大奖赛(休特仑)", 40 | "949": "亚克拉西亚大奖赛(伯尔尼城)", 41 | "7033": "阿拉克尔", 42 | "7005": "邪欲岛", 43 | "7016": "幻觉岛", 44 | "7013": "睡歌岛", 45 | "7018": "蜘蛛岛", 46 | "7014": "杜基岛", 47 | "6007": "和谐之门", 48 | "6001": "航海合作:安亿谷", 49 | "6000": "航海合作:阿尔泰因", 50 | "6002": "航海合作:伯尔尼", 51 | "1002": "疯狂军团", 52 | "1003": "精英·黑暗军团", 53 | "1001": "梦幻军团", 54 | "1004": "疾病军团", 55 | "3008": "混沌的麒麟", 56 | "3005": "西格纳图斯", 57 | "3003": "塔尔西拉", 58 | "3011": "艾拉斯莫", 59 | "6008": "智慧之门", 60 | "5002": "噩梦缠绕:幽灵船", 61 | "6003": "航海合作:罗享达尔", 62 | "7019": "世外桃源", 63 | "1009": "梦幻军团", 64 | "7020": "未知岛", 65 | "3013": "哈尔马格顿", 66 | "3001": "普罗克西玛", 67 | "7035": "基斯布鲁", 68 | "6009": "大地之门", 69 | "6004": "航海合作:约拿", 70 | "1007": "精英·疾病军团", 71 | "3006": "降临的南瓜神", 72 | "3012": "蒂凡尼", 73 | "6010": "忍耐之门", 74 | "6005": "航海合作:佩顿", 75 | "5003": "幽灵徘徊:幽灵船", 76 | "1006": "精英·黑暗军团", 77 | "3014": "恩凯拉杜斯", 78 | "3002": "索尔格兰德", 79 | "6011": "引导之门", 80 | "6006": "航海合作:帕普尼卡", 81 | "1008": "精英·疯狂军团", 82 | "5004": "引发风暴:幽灵船", 83 | "3007": "乌利昂", 84 | "3004": "布雷亚·莱奥斯", 85 | "3015": "莫阿克", 86 | "3016": "Thunderwings", 87 | "8000": "Medeia Capture Event", 88 | "8001": "Slime Island Capture Event", 89 | "2003": "Siege Registration Period", 90 | "2002": "Raid Match Registration Period", 91 | "1010": "Twisting Demon Legion", 92 | "5005": "Sinful Ghost Ship", 93 | "935": "Waterpop Arena", 94 | "938": "Blooming Mokokos!", 95 | "916": "[Event] Festivity Island", 96 | "8006": "Mercenary Join Request", 97 | "9002": "[Assail]Liebertane-[Siege]-[Raid]Preigelli", 98 | "9001": "[Assail]Preigelli-[Siege]-[Raid]Liebertane", 99 | "3022": "Hermut (Liebertane)", 100 | "3021": "Hermut (Preigelli)", 101 | "5006": "Phantom Ghost Ship", 102 | "964": "Steaming Hot Spring (Great Castle)", 103 | "962": "Steaming Hot Spring (Kalaja)", 104 | "961": "Steaming Hot Spring (Luterra Castle)", 105 | "965": "Steaming Hot Spring (Nia Village)", 106 | "959": "Steaming Hot Spring (Rothun)", 107 | "960": "Steaming Hot Spring (Stern)", 108 | "963": "Steaming Hot Spring (Vern Castle)", 109 | "9050": "[Battlefield] Tulubik Battlegrounds", 110 | "8007": "Siege PvP Base Entry" 111 | } 112 | -------------------------------------------------------------------------------- /public/locales/zh/merchantConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "merchant-settings": "Merchant Settings", 3 | "hide-merchant-items": "隐藏商人物品", 4 | "hide-merchant-potential-spawns": "隐藏商人潜在刷新点" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/zh/merchants.json: -------------------------------------------------------------------------------- 1 | { 2 | "next-spawn": "下次刷新", 3 | "potential-spawns": "潜在刷新点(点击城市名查看)", 4 | "location": "位置", 5 | "item": "Item", 6 | "rapport": "好感物品", 7 | "card": "卡牌", 8 | "cooking": "烹饪", 9 | "vote": "投票", 10 | "data-by": "数据来源: ", 11 | "last-updated": "最后更新", 12 | "server-note-text": "注意:当前显示的是服务器时间.", 13 | 14 | "locations": { 15 | "Rethramis": "阿尔忒弥斯", 16 | "Ankumo Mountain": "安格莫斯山麓", 17 | "Log Hill": "罗格希尔", 18 | "Rethramis Border": "边境地带", 19 | "Yudia": "尤迪亚", 20 | "Ozhorn Hill": "奥兹霍恩丘陵", 21 | "Saland Hill": "萨兰德丘陵", 22 | "West Luterra": "卢特兰西部", 23 | "Battlebound Plains": "激战平原", 24 | "Bilbrin Forest": "比尔布林森林", 25 | "Lakebar": "雷科巴", 26 | "Medrick Monastery": "梅德里克修道院", 27 | "Mount Zagoras": "扎格拉斯山", 28 | "East Luterra": "卢特兰东部", 29 | "Dyorika Plain": "迪奥利卡平原", 30 | "Flowering Orchard": "梨树栖息地", 31 | "Sunbright Hill": "圆虹之丘", 32 | "Blackrose Chapel": "黑玫瑰教堂", 33 | "Boreas Domain": "博伦亚领地", 34 | "Croconys Seashore North": "鳄鱼海岸北", 35 | "Croconys Seashore South": "鳄鱼海岸北", 36 | "Leyar Terrace": "拉伊亚阶地", 37 | "Tortoyk": "托托克", 38 | "Forest of Giants": "沉默的巨人森林", 39 | "Seaswept Woods": "海香森林", 40 | "Skyreach Steppe": "岛巨人之森", 41 | "Sweetwater Forest": "甜蜜森林", 42 | "Anikka": "安忆谷", 43 | "Delphi Township": "武县", 44 | "Melody Forest": "木灵森林", 45 | "Prisma Valley": "镜谷", 46 | "Rattan Hill": "藤丘", 47 | "Twilight Mists": "暮光之雾", 48 | "Arthetine": "阿尔泰因", 49 | "Arid Path": "贫瘠通道", 50 | "Nebelhorn": "内伯尔霍伦", 51 | "Riza Falls": "里奇瀑布", 52 | "Scraplands": "分裂之地", 53 | "Totrich": "托特里奇", 54 | "Windbringer Hill": "清风丘陵", 55 | "North Vern": "伯尔尼北部", 56 | "Balankar Mountains": "巴兰卡山脉", 57 | "Fesnar Highland": "佩斯纳尔高原", 58 | "Parna Forest": "帕尔纳森林", 59 | "Port Krona": "克罗纳港", 60 | "Vernese Forest": "维尔尼尔森林", 61 | "Shushire": "休沙瑞", 62 | "Bitterwind Hill": "风刃之丘", 63 | "Frozen Sea": "冰封之海", 64 | "Iceblood Plateau": "霜狱高原", 65 | "Icewing Heights": "冰蝶悬崖", 66 | "Lake Eternity": "结冰时的湖", 67 | "Rohendel": "罗享达尔", 68 | "Breezesome Brae": "风香之丘", 69 | "Elzowins Shade": "埃尔佐温的树荫", 70 | "Glass Lotus Lake": "琉璃莲花湖", 71 | "Lake Shiverwave": "琉璃莲花湖", 72 | "Xeneela Ruins": "荒废的泽纳尔", 73 | "Yorn": "约拿", 74 | "Black Anvil Mine": "黑铁砧车间", 75 | "Hall of Promise": "约定之地", 76 | "Iron Hammer Mine": "铁锤车间", 77 | "Unfinished Garden": "未完成的花园", 78 | "Yorn's Cradle": "起始之地", 79 | "Feiton": "佩顿", 80 | "Kalaja": "佩顿", 81 | "Punika": "帕普尼卡", 82 | "Tideshelf Path": "浅海之路", 83 | "Starsand Beach": "星星沙滩", 84 | "Tikatika Colony": "蒂卡蒂卡群落地", 85 | "Secret Forest": "秘密森林" 86 | }, 87 | "items": { 88 | "920404": { 89 | "name": "Adrenaline-boosting Fluid" 90 | }, 91 | "920701": { "name": "Back Alley Rum" }, 92 | "920803": { 93 | "name": "Raw Boar Meat" 94 | }, 95 | "921004": { "name": "Blood Pudding Chunk" }, 96 | "921405": { "name": "Hairplant" }, 97 | "930605": { "name": "蓝宝石沙丁鱼" }, 98 | "930902": { "name": "令人兴奋的玛卡龙" }, 99 | "931505": { "name": "牛肉干" }, 100 | "931512": { 101 | "name": "Hot Chocolate Coffee" 102 | }, 103 | "63200008": { "name": "Wei" }, 104 | "63200024": { "name": "Thunder Wings" }, 105 | "63200030": { "name": "Mokamoka" }, 106 | "63200035": { "name": "Krause" }, 107 | "63200038": { "name": "Madnick" }, 108 | "63200039": { "name": "Thar" }, 109 | "63200050": { "name": "Gnosis" }, 110 | "63200053": { "name": "Kaysarr" }, 111 | "63200057": { "name": "Kaldor" }, 112 | "63200094": { "name": "Varut" }, 113 | "63200095": { "name": "Prideholme Neria" }, 114 | "63200102": { "name": "Thunder" }, 115 | "63200104": { "name": "Meehan" }, 116 | "63200106": { "name": "Cassleford" }, 117 | "63200108": { "name": "Alifer" }, 118 | "63200111": { "name": "Seria" }, 119 | "63200118": { "name": "Nox" }, 120 | "63200122": { "name": "Eolh" }, 121 | "63200136": { "name": "Stern Neria" }, 122 | "63200144": { "name": "Sian" }, 123 | "63200146": { "name": "Gideon" }, 124 | "63200148": { "name": "Payla" }, 125 | "63200158": { "name": "Lenora" }, 126 | "63200164": { 127 | "name": "Great Castle Neria" 128 | }, 129 | "63200165": { "name": "Piyer" }, 130 | "63200170": { "name": "Levi" }, 131 | "63200171": { "name": "Goulding" }, 132 | "63200174": { "name": "Bergstrom" }, 133 | "63200185": { "name": "Giant Worm" }, 134 | "63200188": { "name": "Cadogan" }, 135 | "63200191": { "name": "Berhart" }, 136 | "63200192": { "name": "Brinewt" }, 137 | "63200197": { "name": "Egg of Creation" }, 138 | "63200205": { "name": "Sir Druden" }, 139 | "63200206": { "name": "Sir Valleylead" }, 140 | "63200209": { "name": "Javern" }, 141 | "63200222": { "name": "Siera" }, 142 | "63200226": { "name": "Morpheo" }, 143 | "63200227": { "name": "Morina" }, 144 | "63200228": { "name": "Madam Moonscent" }, 145 | "63200305": { "name": "Albion" }, 146 | "63200308": { "name": "Seto" }, 147 | "63200309": { "name": "Cicerra" }, 148 | "63200310": { "name": "Stella" }, 149 | "70500000": { "name": "Fancier Boquet" }, 150 | "70500001": { 151 | "name": "莱昂哈特土豆" 152 | }, 153 | "70500002": { 154 | "name": "阿尔忒弥斯圣水" 155 | }, 156 | "70500003": { 157 | "name": "尤迪亚天然盐" 158 | }, 159 | "70500006": { "name": "坚固的鸟笼" }, 160 | "70500007": { 161 | "name": "迪奥利卡草帽" 162 | }, 163 | "70500008": { 164 | "name": "卢特兰之剑仿造品" 165 | }, 166 | "70500011": { "name": "雷科巴西红柿汁" }, 167 | "70500012": { 168 | "name": "阿塞纳佛利姆胸针" 169 | }, 170 | "70500013": { "name": "黑玫瑰" }, 171 | "70500014": { "name": "圆圆的玻璃碎片" }, 172 | "70500015": { "name": "莫可可胡萝卜" }, 173 | "70500016": { 174 | "name": "特大瓢虫娃娃" 175 | }, 176 | "70500024": { "name": "魔法布料" }, 177 | "70500025": { "name": "魔力结晶" }, 178 | "70500026": { 179 | "name": "华丽的音乐盒" 180 | }, 181 | "70500027": { 182 | "name": "骑士团入团申请书" 183 | }, 184 | "70500028": { "name": "Goblin Yarn" }, 185 | "70500033": { "name": "黎明魔石" }, 186 | "70500034": { "name": "精灵羽毛" }, 187 | "70500037": { "name": "帕胡图尔啤酒" }, 188 | "70500038": { "name": "折断的短剑" }, 189 | "70500039": { "name": "生存之书" }, 190 | "70500040": { 191 | "name": "干巴巴的木像" 192 | }, 193 | "70500056": { "name": "丹尼斯布的耳环" }, 194 | "70500059": { "name": "伯顿库尔果实" }, 195 | "70500060": { "name": "皮纳塔制作工具包" }, 196 | "70500061": { 197 | "name": "彩虹蒂卡蒂卡花" 198 | }, 199 | "70501000": { "name": "扑通扑通箱" }, 200 | "70501001": { 201 | "name": "照亮天空的油" 202 | }, 203 | "70501002": { 204 | "name": "铁链战争实录" 205 | }, 206 | "70501003": { 207 | "name": "羞涩的风花粉" 208 | }, 209 | "70501004": { 210 | "name": "姜太公的鱼竿" 211 | }, 212 | "70501005": { "name": "高级留声机" }, 213 | "70501006": { 214 | "name": "伯尔尼建国纪念币" 215 | }, 216 | "70501007": { 217 | "name": "赛尔斯圣经" 218 | }, 219 | "70501008": { "name": "红月眼泪" }, 220 | "70501010": { 221 | "name": "西琳女王的祝福" 222 | }, 223 | "70501011": { "name": "Oreha Viewing Stone" }, 224 | "70503000": { 225 | "name": "比武大会参赛证" 226 | }, 227 | "70503001": { "name": "Yudia Spellbook" }, 228 | "70503005": { "name": "Shimmering Essence" }, 229 | "70503006": { "name": "Energy X7 Capsule" }, 230 | "70503008": { "name": "Piyer's Secret Textbook" } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/vercel_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/public/vercel_favicon.ico -------------------------------------------------------------------------------- /should_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF" 4 | 5 | if [[ "$VERCEL_GIT_COMMIT_REF" == "develop" || "$VERCEL_GIT_COMMIT_REF" == "main" ]] ; then 6 | # Proceed with the build 7 | echo "✅ - Build can proceed" 8 | exit 1; 9 | 10 | else 11 | # Don't build 12 | echo "🛑 - Build cancelled" 13 | exit 0; 14 | fi -------------------------------------------------------------------------------- /sound.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mp3' { 2 | const src: string 3 | export default src 4 | } 5 | -------------------------------------------------------------------------------- /sounds/alert_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_1.mp3 -------------------------------------------------------------------------------- /sounds/alert_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_2.mp3 -------------------------------------------------------------------------------- /sounds/alert_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_3.mp3 -------------------------------------------------------------------------------- /sounds/alert_4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_4.mp3 -------------------------------------------------------------------------------- /sounds/alert_5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_5.mp3 -------------------------------------------------------------------------------- /sounds/alert_6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwjoshuak/lostarktimer.app-web/0e0584f3fc9a91d024a22245a7873a0d853b7e1c/sounds/alert_6.mp3 -------------------------------------------------------------------------------- /sounds/index.js: -------------------------------------------------------------------------------- 1 | const alert1 = require('./alert_1.mp3') 2 | const alert2 = require('./alert_2.mp3') 3 | const alert3 = require('./alert_3.mp3') 4 | const alert4 = require('./alert_4.mp3') 5 | const alert5 = require('./alert_5.mp3') 6 | const alert6 = require('./alert_6.mp3') 7 | 8 | export { alert1, alert2, alert3, alert4, alert5, alert6 } 9 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './pages/**/*.{js,ts,jsx,tsx}', 4 | './components/**/*.{js,ts,jsx,tsx}', 5 | './util/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require('daisyui')], 12 | daisyui: { 13 | themes: ['light', 'dark'], 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /util/createTableData.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { GameEvent } from '../common' 3 | import { GameEventTableCell } from '../components' 4 | import { v4 as uuidv4 } from 'uuid' 5 | import { DateTime, Interval, Zone } from 'luxon' 6 | import useLocalStorage from '@olerichter00/use-localstorage' 7 | import { MerchantData } from './types/types' 8 | import MerchantTableCell from '../components/MerchantTableCell' 9 | import classNames from 'classnames' 10 | import WanderingMerchant from '../common/WanderingMerchant' 11 | interface TableProps { 12 | serverTime: DateTime 13 | currDate: DateTime 14 | viewLocalizedTime: boolean 15 | view24HrTime: boolean 16 | } 17 | interface CreateGameEventTableProps extends TableProps { 18 | events: Array 19 | isGameEvent: true 20 | } 21 | interface CreateMerchantTableProps extends TableProps { 22 | events: Array 23 | isGameEvent: false 24 | } 25 | function isGameEventData(object: any): object is CreateGameEventTableProps { 26 | return object.isGameEvent || object instanceof GameEvent 27 | } 28 | export const createTableData = ( 29 | props: CreateGameEventTableProps | CreateMerchantTableProps 30 | ) => { 31 | const { events, serverTime, currDate, viewLocalizedTime, view24HrTime } = 32 | props 33 | let arr: React.ReactElement[] = [] 34 | for (let i = 0; i < events.length; i += 2) { 35 | let children: ReactElement[] = [] 36 | for ( 37 | let j = 0; 38 | i + j < (i + 2 < events.length ? i + 2 : events.length); 39 | j++ 40 | ) { 41 | if (props.isGameEvent) { 42 | let evt = events[i + j] as GameEvent 43 | children.push( 44 | 51 | ) 52 | } else { 53 | let evt = events[i + j] as WanderingMerchant 54 | children.push( 55 | 62 | ) 63 | } 64 | } 65 | if (children.length == 1) { 66 | children.push( 67 | 71 | ) 72 | } 73 | arr.push( 74 | 75 | {children} 76 | 77 | ) 78 | } 79 | return arr 80 | } 81 | 82 | export const generateTimestampStrings = ( 83 | event: GameEvent | WanderingMerchant, 84 | // eventTimes: Interval[], 85 | interval: Interval, 86 | serverTime: DateTime, 87 | localizedTZ: Zone, 88 | view24HrTime: boolean, 89 | // timeDiff: number, 90 | idx: number 91 | ) => { 92 | let eventTimes = 93 | (event as WanderingMerchant).schedule || (event as GameEvent).times 94 | let diff = interval.start.diff(serverTime).valueOf() 95 | let inProgress = 96 | interval.start.diff(serverTime).toMillis() < 0 && 97 | interval.end.diff(serverTime).toMillis() > 0 98 | let startTime = interval.start 99 | .setZone(localizedTZ) 100 | .toLocaleString( 101 | view24HrTime ? DateTime.TIME_24_SIMPLE : DateTime.TIME_SIMPLE 102 | ) 103 | let endTime = interval.end 104 | .setZone(localizedTZ) 105 | .toLocaleString( 106 | view24HrTime ? DateTime.TIME_24_SIMPLE : DateTime.TIME_SIMPLE 107 | ) 108 | return ( 109 | 110 | = 0 && diff < 900000), 115 | 'text-green-700 dark:text-success': diff >= 900000, 116 | })} 117 | > 118 | {startTime} 119 | {!interval.isEmpty() ? ` - ${endTime}` : ''} 120 | 121 | 126 | {idx < eventTimes.length - 1 ? ' / ' : ''} 127 | 128 | 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /util/static.ts: -------------------------------------------------------------------------------- 1 | import { RegionKey } from './types/types' 2 | 3 | export const RegionTimeZoneMapping: { [K in RegionKey]: string } = { 4 | 'US West': 'UTC-7', 5 | 'US East': 'UTC-4', 6 | 'EU Central': 'UTC+1', 7 | 'EU West': 'UTC-0', 8 | 'South America': 'UTC-4', 9 | } 10 | -------------------------------------------------------------------------------- /util/types/types.ts: -------------------------------------------------------------------------------- 1 | import { Interval } from 'luxon' 2 | import regions from '../../data/regions.json' 3 | export interface MerchantData { 4 | name: string 5 | continent: string 6 | schedule: number 7 | items: { rapport: number[]; cards: number[]; cooking: number[] } 8 | uuid: string 9 | times: Interval[] 10 | } 11 | 12 | export interface MerchantAPIData { 13 | _id: string 14 | region: string 15 | server: string 16 | location: string 17 | item: string 18 | name: string 19 | } 20 | 21 | export type RegionKey = 22 | | keyof typeof regions 23 | | 'US West' 24 | | 'US East' 25 | | 'EU West' 26 | | 'EU Central' 27 | | 'South America' 28 | 29 | export type ServerKey = 30 | | 'Mari' 31 | | 'Valtan' 32 | | 'Enviska' 33 | | 'Akkan' 34 | | 'Bergstrom' 35 | | 'Shandi' 36 | | 'Rohendel' 37 | | 'Azena' 38 | | 'Una' 39 | | 'Regulus' 40 | | 'Avesta' 41 | | 'Galatur' 42 | | 'Karta' 43 | | 'Ladon' 44 | | 'Kharmine' 45 | | 'Elzowin' 46 | | 'Sasha' 47 | | 'Adrinne' 48 | | 'Aldebaran' 49 | | 'Zosma' 50 | | 'Vykas' 51 | | 'Danube' 52 | | 'Neria' 53 | | 'Kadan' 54 | | 'Trixion' 55 | | 'Calvasus' 56 | | 'Thirain' 57 | | 'Zinnervale' 58 | | 'Asta' 59 | | 'Wei' 60 | | 'Slen' 61 | | 'Sceptrum' 62 | | 'Procyon' 63 | | 'Beatrice' 64 | | 'Inanna' 65 | | 'Thaemine' 66 | | 'Sirius' 67 | | 'Antares' 68 | | 'Brelshaza' 69 | | 'Nineveh' 70 | | 'Mokoko' 71 | | 'Rethramis' 72 | | 'Tortoyk' 73 | | 'Moonkeep' 74 | | 'Stonehearth' 75 | | 'Shadespire' 76 | | 'Tragon' 77 | | 'Petrania' 78 | | 'Punika' 79 | | 'Kazeros' 80 | | 'Agaton' 81 | | 'Gienah' 82 | | 'Arcturus' 83 | | 'Yorn' 84 | | 'Feiton' 85 | | 'Vern' 86 | | 'Kurzan' 87 | | 'Prideholme' 88 | -------------------------------------------------------------------------------- /util/usePrevious.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | const usePrevious = (value: T): T | undefined => { 4 | const ref = useRef() 5 | useEffect(() => { 6 | ref.current = value 7 | }) 8 | return ref.current 9 | } 10 | 11 | export default usePrevious 12 | --------------------------------------------------------------------------------