-
-
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 | |
| ) | |
| ) | |
| } |
Instead of ObservationEvent it could be more useful to have a flag isObservingEvent or isFriendEvent in each TripItemto 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
zodfor 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+1Timeline 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

Uh oh!
There was an error while loading. Please reload this page.