-
-
Save MrModest/9bdd7651cc62d240f3f5874473bf6e98 to your computer and use it in GitHub Desktop.
/* | |
The idea behind it to make something like 'TravelerBuddy', but simplier. Easy to write and easy to read. | |
Just to have a chronological plan of points of interests and all crucial parts of journey. Also, to have all crucial info handy. | |
Backend is as simple as a CRUD (if you aren't planned to implement multi-user/authorization support). | |
The most work is expected on Frondend side. It needs to provide a convenient and very specific form for each TripItem. | |
And also draw a "graph" like below. | |
No need to support autocompletion for Address. It's enought to support "open in Map" feature. So even no needs in having a build-in map view. | |
Since there's no much commons between trip items (as well as no joins between them), | |
it could make sense to use a NoSQL rather than have a ton of nullable columns in one RDS table. | |
10:00 -|- Leave the house | |
| | |
10:30 -|- Departure from bus station | |
| | |
11:30 -|- Arrival to BER airport | |
| | |
| | |
13:30 -|- Flight departure | |
... | |
*/ | |
data class TimezonedDateTime ( // very important to store timezone since trips could be between timezones | |
val datetime: LocalDateTime, | |
val localTz: TimeZone | |
) { | |
fun toTz(val tz: TimeZone): LocalDateTime { // to show time according to the current timezone of the viewer | |
// ... | |
} | |
} | |
data class Trip( // simple container for trip items | |
val title: String, | |
val items: List<TripItem> | |
val start: TimezonedDateTime, | |
val end: TimezonedDateTime | |
) | |
data class DocumentLink( | |
val name: String, | |
val link: String // GoogleDrive Link, for example | |
) | |
data class TimelinePoint ( | |
val name: String, | |
val time: TimezonedDatetime, | |
val address: Address | |
) | |
interface TripItem { // abstract entity to represent different type of components of the trip | |
val id: UUID, | |
val note: String, // markdown - to support reference links | |
val attachments: List<DocumentLink>, | |
val timelinePoints: List<TimelinePoint> | |
} // all these fields should be represented in any inherited classes, ommited just for simplisity | |
data class Person ( // could as participant of the trip as well as any involved person. | |
val name: String, | |
val contact: String, | |
val note: String | |
) | |
data class MapPoint( | |
val longitude: String, | |
val latitude: String | |
) | |
data class Address( | |
val country: String, | |
val city: String, | |
val address: String, | |
val mapPoint: MapPoint | |
) { | |
val None: Address = Address("None", "None", "None", MapPoint("0", "0")) | |
} | |
enum class AirportCode { | |
LEJ, AYT //, etc.. | |
} | |
data class Airport( | |
val code: AirportCode, | |
val name: String, | |
val address: Address | |
) | |
data class AirportPoint( | |
val airport: Airport, | |
val terminal: String, | |
val gate: String, | |
val time: TimezonedDateTime, | |
) | |
data class Flight: TripItem ( | |
val flightNumber: String, | |
val carrier: String, | |
val bookingCode: String, | |
val seat: String, | |
val passengers: List<Person>, | |
val departure: AirportPoint, | |
val arrival: AirportPoint | |
// (?) connected (next) flight with type `Flight` | |
// (?) return flight with type `Flight` | |
// similar for LongLandTransfer | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Flight from [${departure.airport.code}]", | |
departure.time, | |
departure.airport.address | |
), | |
TimelinePoint( | |
"Flight to [${arrival.airport.code}]", | |
arrival.time, | |
arrival.airport.address | |
) | |
) | |
} | |
data class Hotel: TripItem ( | |
val name: String, | |
val address: Address, | |
val reservationOn: Person, // the person who need to contact with hotel | |
val guests: List<Person>, | |
val numberOfRooms: Int, | |
val contacts: String // phone; email | |
val checkIn: TimezonedDateTime, // it's about planned time, not available from hotel | |
val checkOut: TimezonedDateTime // for example, hotel offers check out from 11:00, but you want to leave at 8:00. So here should be set 8:00. | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Check-In [${name}]", | |
checkIn, | |
address | |
), | |
TimelinePoint( | |
"Check-Out [${name}]", | |
checkIn, | |
address | |
) | |
) | |
} | |
data class PublicTransportConnections( // BUS 165 -> S 45 -> RE 5(3345) | |
val time: TimezonedDateTime, // [{ time: "2024-01-10 09:00", decriptions: "BUS 165" }, { time: "2024-01-10 10:00", decriptions: "S 45" }, { time: "2024-01-10 11:30", decriptions: "RE 5(3345)" }] | |
val description: String, | |
val point: Address = Address.None // no need to bother filling it every time | |
) | |
data class PublicTransport: TripItem ( // includes all potential changes, so use only one item even if you need to change from bus to train and then to tram. | |
val startPoint: Address, | |
val endPoint: Address, | |
val departure: TimezonedDateTime, // could be approximate | |
val arrival: TimezonedDateTime, // could be approximate | |
val approximateDuration: String, // 1h 13 min | |
val connections: PublicTransportConnections[], // optional | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Start commute to '${endPoint.address}' [${approximateDuration}]", | |
departure, | |
startPoint | |
), | |
*connections.map { | |
TimelinePoint( | |
it.description, | |
it.time, | |
it.point | |
) | |
}, | |
TimelinePoint( | |
"End commute to '${endPoint.address}' [${approximateDuration}]", | |
arrival, | |
endPoint | |
) | |
) | |
} | |
enum class TransferType { | |
Bus, Train, Ferry | |
} | |
data class LongLandTransfer: TripItem ( // it's about long transfers by land like train or intercity buses. Somethings where punctuality is crucial | |
val transferType: TransferType, | |
val transferNumber: String, | |
val carrier: String, | |
val contacts: String, | |
val passengers: List<Person> | |
val pickUpTime: TimezonedDateTime, | |
val pickUpLocation: Address, | |
val dropOffTime: TimezonedDateTime, | |
val dropOffLocation: Address | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"[${carrier}] ${transferType} from ${pickUpLocation.address}", | |
pickUpTime, | |
pickUpLocation | |
), | |
TimelinePoint( | |
"[${carrier}] ${transferType} to ${dropOffLocation.address}", | |
dropOffTime, | |
dropOffLocation | |
) | |
) | |
} | |
data class GeneralPoint: TripItem ( // any point you want to mark on your journey map. For example, the very first and very last points of your trip. | |
val name: String, | |
val address: Address, | |
val beHereAt: TimezonedDateTime | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Be at ${address.address}", | |
beHereAt, | |
address | |
) | |
) | |
} | |
data class ObservationEvent: TripItem ( // not related to us, but important to observe, like friend's flight | |
val name: String, | |
val startTime: TimezonedDateTime, | |
val endTime: TimezonedDateTime, | |
val address: Address? | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Start [${name}]", | |
startTime, | |
address | |
), | |
TimelinePoint( | |
"End [${name}]", | |
endTime, | |
address | |
) | |
) | |
} | |
data class Visiting: TripItem ( // visiting friends or places | |
val description: String, | |
val startTime: TimezonedDateTime, | |
val endTime: TimezonedDateTime, | |
val address: Address | |
val persons: List<Person> // relevant if visiting someone, empty if no ther people involved | |
) { | |
override val timelinePoints: List<TimelinePoint> | |
get = listOf( | |
TimelinePoint( | |
"Start [${description}]", | |
startTime, | |
address | |
), | |
TimelinePoint( | |
"End [${description}]", | |
endTime, | |
address | |
) | |
) | |
} |
MrModest
commented
Jan 8, 2024
•
Instead of ObservationEvent
it could be more useful to have a flag isObservingEvent
or isFriendEvent
in each TripItem
to mark that this particular item isn't belongs to the participants of the trip directly.
So
interface TripItem {
val id: UUID,
val note: String,
val attachments: List<DocumentLink>,
val timelinePoints: List<TimelinePoint>,
val isObservingEvent: Bool
}
or
data class TripItem<TMetadata : TripItemMetadata> {
val id: UUID,
val note: String,
val attachments: List<DocumentLink>,
val metadata: TMetadata,
val isObservingEvent: Bool
}
https://github.com/frappe/gantt
Framework independent library. Can be used for a Grist custom widget

data class Timeline( // Should be a property for a `TripItem` instead of "ObeservationEvent" flag.
val id: String,
val name: String,
val blockColor: Color, // or Enum or HEX string
val backgroundColor: Color,
val order: Int // For example, "Major transfers" is "1", and "Friend 2 - Transfers" is "4"
)
More examples for dedicated timeline:
- Car rental time
- Overall vacation days (to be aware of the possibility of adjustments in the trip.)
- Your cat nanny visits (to be able to ask whether your cat was fed while you're in the trip)
https://github.com/namespace-ee/react-calendar-timeline - allows more than one block in one timeline
Add property type
to the GeneralPoint
to have enums like: Cafe, Viewpoint, Leisure, Car parked place, car rent office, custom. For custom you can configure the name and icon (?).
Technologies suggestions:
- Next.js with React Server Components
- StyleX (or Tailwind) as CSS solution
zod
for external input validation- ReactQuery for fetching data
- Docker compose
- SQLite (or PostgreSQL) as DB
- GH Actions as CI and uploading docker image
Links:
- https://habr.com/ru/articles/781166/
- https://youtu.be/AeQ3f4zmSMs
- https://github.com/vercel/next-react-server-components/tree/main/components
Note: should also work completely offline (PWA?). Then can I still use server components?
Idea: add expense array to all TripItems with 4 fields: amount, currency, category (food/transportation, etc.) and note.
For sync, each item (and derivatives) should have 'syncData` property with following fields: createdDate, updatedDate, createdDeviceName, updatedDeviceName.
Keep an individual check-list per trip item. And then provide with one accumulated check-list.
For example, under the flight you can have "take passport" and "check hand luggage restrictions compliancy". But for a commute from airport to the hotel - "take discount voucher for airport rail express that I handed over from my friend".
And all 3 stuff user can see in one combined place as requirements before the trip starts
---
title: Antalya (Fethiye)
start:
date: 2024-02-02
timezone: UTC+1
end:
date: 2024-02-13
timezone: UTC+1
items:
- id: d70e08f8-6504-4400-a0d4-15f338f8a043
type: PublicTansport
note: Commute from Home to the hotel in Leipzig
attachments: []
metadata:
startPoint:
country: Germany
city: Berlin
address: Kastanienallee 21, 12500 Berlin
mapPoint:
longitude: '72.4544810685576'
latitude: '41.51634385867111'
endPoint:
country: Germany
city: Leipzig
address: Leipzig Hbf
mapPoint:
longitude: '92.4544810685576'
latitude: '93.51634385867111'
departure:
date: 2024-02-02
time: 19:45
timezone: UTC+1
arrival:
date: 2024-02-02
time: 22:43
timezone: UTC+1
approximateDuration: 1h 13 min
commuteDescription: BUS 165 -> S 45 -> RE 5(3345)
- id: ff6476ec-da38-4c74-9bb6-df33f2e382ff
type: Hotel
note: Hotel in Leipzig
attachments:
- name: Booking_1234.pdf
link: https://drive.google.com/path/to/file.pdf
metadata:
hotelName:
address:
country: Germany
city: Leipzig
address: Kastanstraße 42, 12211 Leipzig
mapPoint:
longitude: '62.4544456785576'
latitude: '33.51634444867111'
reservationOn:
name: Katrin Mustermann
contact: +49 177 1234567
note: sample note
guests:
- name: Katrin Mustermann
contact: +49 177 1234567
note: sample note
- name: Mark Mustermann
contact: +49 177 2345678
numberOfRooms: 1
checkIn:
date: 2024-02-02
time: 22:50
timezone: UTC+1
checkOut:
date: 2024-02-03
time: 15:00
timezone: UTC+1
Timeline design guildlines: https://experience.sap.com/fiori-design-android/timeline-view/
Instead of the month in the gray line, we can show timezone change warning

P.S.: Made in Lunacy