Skip to content

Instantly share code, notes, and snippets.

@MrModest
Last active April 7, 2024 10:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrModest/9bdd7651cc62d240f3f5874473bf6e98 to your computer and use it in GitHub Desktop.
Save MrModest/9bdd7651cc62d240f3f5874473bf6e98 to your computer and use it in GitHub Desktop.
The model structure for potential trip planner app like 'tripit.com' or 'travelerbuddy.com'.
/*
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
Copy link
Author

MrModest commented Jan 8, 2024

data class TimelinePoint (
    val name: String,
    val time: TimezonedDatetime,
    val address: Address
)

data class TripItem<TMetadata : TripItemMetadata> { // with this structure we can use RDS
    val id: UUID,
    val note: String,
    val attachments: List<DocumentLink>,
    val metadata: TMetadata // since we use it only to generate timeline (only once) and showing in the UI - it could be stored in JSON column.
                         // Use it as a joined table would be problematic since there're different types of metadata
} {
    val timelinePoints: TimeInterval // even so it generated, does it make sense to store it in the DB in a dedicated column?
        get = metadata.getTimelinePoints()
}

data class TimeInterval (
    val start: TimelinePoint,
    val end: TimelinePoint
)

interface TripItemMetadata {
    fun getTimelinePoints(): TimeInterval
}

data class Flight : TripItemMetadata (
    val flightNumber: String,
    val carrier: String,
    val bookingCode: String,
    val seat: String,
    val passengers: List<Person>,
    val departure: AirportPoint,
    val arrival: AirportPoint
) {
    fun getTimelinePoints(): TimeInterval =
        TimeInterval(
           TimelinePoint(
               "Flight [${departure.airport.code}]",
               departure.time,
               departure.airport.address
           ),
           TimelinePoint(
               "Flight [${arrival.airport.code}]",
               arrival.time,
               arrival.airport.address
           )
       )
}

// ...

val flight = TripItem<Flight>(
    UUID.generateId(),
    "Sample note",
    emptyList(),
    Flight(...)
)

val tlPoints = flight.timelinePoints

@kamilmodest
Copy link

kamilmodest commented Jan 15, 2024

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
}

@MrModest
Copy link
Author

MrModest commented Jan 15, 2024

https://github.com/frappe/gantt

Framework independent library. Can be used for a Grist custom widget

@kamilmodest
Copy link

kamilmodest commented Jan 16, 2024

image
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)

@kamilmodest
Copy link

kamilmodest commented Jan 16, 2024

https://github.com/namespace-ee/react-calendar-timeline - allows more than one block in one timeline

@MrModest
Copy link
Author

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 (?).

@MrModest
Copy link
Author

MrModest commented Feb 10, 2024

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:

Note: should also work completely offline (PWA?). Then can I still use server components?

@MrModest
Copy link
Author

MrModest commented Feb 10, 2024

Idea: add expense array to all TripItems with 4 fields: amount, currency, category (food/transportation, etc.) and note.

@MrModest
Copy link
Author

For sync, each item (and derivatives) should have 'syncData` property with following fields: createdDate, updatedDate, createdDeviceName, updatedDeviceName.

@MrModest
Copy link
Author

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

@MrModest
Copy link
Author

MrModest commented Apr 1, 2024

---
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

@MrModest
Copy link
Author

MrModest commented Apr 7, 2024

Timeline design guildlines: https://experience.sap.com/fiori-design-android/timeline-view/

image

Instead of the month in the gray line, we can show timezone change warning
image

@MrModest
Copy link
Author

MrModest commented Apr 7, 2024

image

P.S.: Made in Lunacy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment