feat: add start_code and end_code fields to Transportation model and update related components

This commit is contained in:
Sean Morley
2025-12-27 15:26:50 -05:00
parent fd463b428b
commit 65fcd94898
12 changed files with 461 additions and 146 deletions

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.6 on 2025-12-27 00:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0064_collectionitineraryitem'),
]
operations = [
migrations.AddField(
model_name='transportation',
name='end_code',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='transportation',
name='start_code',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -316,6 +316,8 @@ class Transportation(models.Model):
origin_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
start_code = models.CharField(max_length=100, blank=True, null=True) # Could be airport code, station code, etc.
end_code = models.CharField(max_length=100, blank=True, null=True) # Could be airport code, station code, etc.
to_location = models.CharField(max_length=200, blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)

View File

@@ -432,7 +432,7 @@ class TransportationSerializer(CustomModelSerializer):
'link', 'date', 'flight_number', 'from_location', 'to_location',
'is_public', 'collection', 'created_at', 'updated_at', 'end_date',
'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude',
'start_timezone', 'end_timezone', 'distance', 'images', 'attachments'
'start_timezone', 'end_timezone', 'distance', 'images', 'attachments', 'start_code', 'end_code'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'distance']

View File

@@ -9,6 +9,14 @@
import MarkdownEditor from './MarkdownEditor.svelte';
// Icons
import CollectionIcon from '~icons/mdi/folder-multiple';
import InfoIcon from '~icons/mdi/information';
import CalendarIcon from '~icons/mdi/calendar';
import LinkIcon from '~icons/mdi/link';
import SaveIcon from '~icons/mdi/content-save';
import CloseIcon from '~icons/mdi/close';
export let collectionToEdit: Collection | null = null;
let collection: Collection = {
@@ -22,7 +30,9 @@
locations: collectionToEdit?.locations || [],
link: collectionToEdit?.link || '',
shared_with: undefined,
itinerary: []
itinerary: [],
status: 'folder',
days_until_start: null
};
console.log(collection);
@@ -107,141 +117,226 @@
}
</script>
<dialog id="my_modal_1" class="modal">
<dialog id="my_modal_1" class="modal backdrop-blur-sm">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{collectionToEdit ? $t('adventures.edit_collection') : $t('collection.new_collection')}
</h3>
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.basic_information')}
<div
class="modal-box w-11/12 max-w-6xl bg-gradient-to-br from-base-100 via-base-100 to-base-200 border border-base-300 shadow-2xl max-h-[85vh] flex flex-col"
role="dialog"
on:keydown={handleKeydown}
tabindex="0"
>
<!-- Header Section -->
<div
class="top-0 z-10 bg-base-100/90 backdrop-blur-lg border-b border-base-300 -mx-6 -mt-6 px-6 py-4 mb-6"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary/10 rounded-xl">
<CollectionIcon class="w-8 h-8 text-primary" />
</div>
<div class="collapse-content">
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={collection.name}
class="input input-bordered w-full"
required
/>
<div>
<h1 class="text-3xl font-bold text-primary bg-clip-text">
{collectionToEdit
? $t('adventures.edit_collection')
: $t('collection.new_collection')}
</h1>
<p class="text-sm text-base-content/60">
{collectionToEdit
? $t('collection.update_collection_details')
: $t('collection.create_new_collection')}
</p>
</div>
</div>
<!-- Close Button -->
<button class="btn btn-ghost btn-square" on:click={close}>
<CloseIcon class="w-5 h-5" />
</button>
</div>
</div>
<!-- Main Content -->
<div class="p-6 overflow-auto max-h-[70vh]">
<form method="post" on:submit={handleSubmit} class="space-y-6">
<!-- Basic Information Section -->
<div class="card bg-base-100 border border-base-300 shadow-lg">
<div class="card-body p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-primary/10 rounded-lg">
<InfoIcon class="w-5 h-5 text-primary" />
</div>
<h2 class="text-xl font-bold">{$t('adventures.basic_information')}</h2>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={collection.description} editor_height={'h-32'} />
</div>
<!-- Start Date -->
<div>
<label for="start_date">{$t('adventures.start_date')}</label>
<input
type="date"
id="start_date"
name="start_date"
bind:value={collection.start_date}
class="input input-bordered w-full"
/>
</div>
<!-- End Date -->
<div>
<label for="end_date">{$t('adventures.end_date')}</label>
<input
type="date"
id="end_date"
name="end_date"
bind:value={collection.end_date}
class="input input-bordered w-full"
/>
</div>
<!-- Public -->
<div>
<label class="label cursor-pointer flex items-start space-x-2">
<span class="label-text">{$t('collection.public_collection')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={collection.is_public}
/>
</label>
</div>
<!-- Link -->
<div>
<label for="link">{$t('adventures.link')}</label>
<input
type="text"
id="link"
name="link"
bind:value={collection.link}
class="input input-bordered w-full"
/>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Left Column -->
<div class="space-y-4">
<!-- Name Field -->
<div class="form-control">
<label class="label" for="name">
<span class="label-text font-medium"
>{$t('adventures.name')}<span class="text-error ml-1">*</span></span
>
</label>
<input
type="text"
id="name"
name="name"
bind:value={collection.name}
class="input input-bordered w-full"
placeholder={$t('collection.enter_collection_name')}
required
/>
</div>
<!-- Description Field -->
<div class="form-control">
<label class="label" for="description">
<span class="label-text font-medium">{$t('adventures.description')}</span>
</label>
<MarkdownEditor bind:text={collection.description} editor_height={'h-32'} />
</div>
<!-- Link Field -->
<div class="form-control">
<label class="label" for="link">
<span class="label-text font-medium flex items-center gap-2">
<LinkIcon class="w-4 h-4" />
{$t('adventures.link')}
</span>
</label>
<input
type="text"
id="link"
name="link"
bind:value={collection.link}
class="input input-bordered w-full"
placeholder="https://example.com"
/>
</div>
</div>
<!-- Right Column -->
<div class="space-y-4">
<!-- Start Date -->
<div class="form-control">
<label class="label" for="start_date">
<span class="label-text font-medium flex items-center gap-2">
<CalendarIcon class="w-4 h-4" />
{$t('adventures.start_date')}
</span>
</label>
<input
type="date"
id="start_date"
name="start_date"
bind:value={collection.start_date}
class="input input-bordered w-full"
/>
</div>
<!-- End Date -->
<div class="form-control">
<label class="label" for="end_date">
<span class="label-text font-medium flex items-center gap-2">
<CalendarIcon class="w-4 h-4" />
{$t('adventures.end_date')}
</span>
</label>
<input
type="date"
id="end_date"
name="end_date"
bind:value={collection.end_date}
class="input input-bordered w-full"
/>
</div>
<!-- Public Toggle -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={collection.is_public}
/>
<span class="label-text font-medium">{$t('collection.public_collection')}</span>
</label>
<div class="pl-12">
<span class="text-sm text-base-content/60"
>{$t('collection.public_collection_description')}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Date Warning Alert -->
{#if !collection.start_date && !collection.end_date}
<div class="mt-4">
<div role="alert" class="alert alert-neutral">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>{$t('adventures.collection_no_start_end_date')}</span>
<div role="alert" class="alert alert-info shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>{$t('adventures.collection_no_start_end_date')}</span>
</div>
{/if}
<!-- Share Link Section (only if public and has ID) -->
{#if collection.is_public && collection.id}
<div class="card bg-base-100 border border-base-300 shadow-lg">
<div class="card-body p-6">
<h3 class="font-semibold text-lg mb-3">{$t('adventures.share_collection')}</h3>
<div class="flex items-center gap-3">
<input
type="text"
value="{window.location.origin}/collections/{collection.id}"
readonly
class="input input-bordered flex-1 font-mono text-sm"
/>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(
`${window.location.origin}/collections/${collection.id}`
);
addToast('success', $t('adventures.link_copied'));
}}
class="btn btn-primary gap-2"
>
<LinkIcon class="w-4 h-4" />
{$t('adventures.copy_link')}
</button>
</div>
</div>
</div>
{/if}
<div class="mt-4">
<button type="submit" class="btn btn-primary">
{$t('notes.save')}
</button>
<button type="button" class="btn" on:click={close}>
<!-- Action Buttons -->
<div class="flex gap-3 justify-end pt-4">
<button type="button" class="btn btn-neutral gap-2" on:click={close}>
<CloseIcon class="w-5 h-5" />
{$t('about.close')}
</button>
<button type="submit" class="btn btn-primary gap-2">
<SaveIcon class="w-5 h-5" />
{$t('notes.save')}
</button>
</div>
{#if collection.is_public && collection.id}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm text-neutral-content">
<p class=" font-semibold">{$t('adventures.share_collection')}</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono">
{window.location.origin}/collections/{collection.id}
</p>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(
`${window.location.origin}/collections/${collection.id}`
);
}}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2"
>
{$t('adventures.copy_link')}
</button>
</div>
</div>
{/if}
</form>
</div>
</div>

View File

@@ -183,12 +183,11 @@
<!-- Content -->
<div class="card-body p-4 space-y-3">
<!-- Title -->
<button
on:click={() => goto(`/collections/${collection.id}`)}
class="text-lg font-semibold text-left hover:text-primary transition-colors duration-200 line-clamp-2 group-hover:underline"
<a
href="/collections/{collection.id}"
class="hover:text-primary transition-colors duration-200 line-clamp-2 text-lg font-semibold"
>{collection.name}</a
>
{collection.name}
</button>
<!-- Stats -->
<div class="flex flex-wrap items-center gap-2 text-sm text-base-content/70">

View File

@@ -1235,7 +1235,12 @@
<Bed class="w-4 h-4" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{lodging.name}</p>
<a
href={`/lodging/${lodging.id}`}
class="hover:text-primary transition-colors duration-200 line-clamp-2 text-md font-semibold"
>
{lodging.name}
</a>
{#if lodging.location}
<p class="text-xs opacity-60 truncate">{lodging.location}</p>
{/if}

View File

@@ -55,6 +55,8 @@
lng: number;
location: string;
} | null = null;
export let initialStartCode: string | null = null;
export let initialEndCode: string | null = null;
let isSearching = false;
let searchResults: GeoSelection[] = [];
@@ -68,6 +70,10 @@
let initialApplied = false;
let initialTransportationApplied = false;
// Track any provided codes (airport / station / etc)
let startCode: string | null = null;
let endCode: string | null = null;
// track previous airport mode to detect toggles
let prevAirportMode = airportMode;
@@ -91,6 +97,8 @@
startMarker = null;
endMarker = null;
startLocationData = null;
startCode = null;
endCode = null;
endLocationData = null;
}
@@ -115,7 +123,7 @@
selectedMarker = { lng: selection.lng, lat: selection.lat };
mapCenter = [selection.lng, selection.lat];
mapZoom = 14;
searchQuery = selection.name || '';
searchQuery = selection.location || selection.name || '';
displayName = selection.location || selection.name;
await performDetailedReverseGeocode(selection.lat, selection.lng);
}
@@ -129,7 +137,9 @@
location: initialStartLocation.location
};
startMarker = { lng: initialStartLocation.lng, lat: initialStartLocation.lat };
startSearchQuery = initialStartLocation.name;
startCode =
initialStartCode || deriveCode(initialStartLocation.name, initialStartLocation.name);
startSearchQuery = startCode || initialStartLocation.location || initialStartLocation.name;
await performDetailedReverseGeocode(
initialStartLocation.lat,
initialStartLocation.lng,
@@ -145,7 +155,8 @@
location: initialEndLocation.location
};
endMarker = { lng: initialEndLocation.lng, lat: initialEndLocation.lat };
endSearchQuery = initialEndLocation.name;
endCode = initialEndCode || deriveCode(initialEndLocation.name, initialEndLocation.name);
endSearchQuery = endCode || initialEndLocation.location || initialEndLocation.name;
await performDetailedReverseGeocode(initialEndLocation.lat, initialEndLocation.lng, 'end');
}
@@ -273,6 +284,28 @@
}, 300);
}
function deriveCode(value: string | undefined, fallback?: string): string | null {
if (!value && !fallback) return null;
const match = value?.match(/\b([A-Z0-9]{3,5})\b/) || value?.match(/\(([A-Z0-9]{3,5})\)/);
if (match && match[1]) return match[1].toUpperCase();
const candidate = (fallback || '').trim();
if (candidate.length && candidate.length <= 5) return candidate.toUpperCase();
return null;
}
function resolveCode(selection: GeoSelection | null, typedQuery: string): string | null {
// Prefer explicit user-typed code when in airport mode
const fromTyped = deriveCode(typedQuery, typedQuery);
if (fromTyped) return fromTyped;
if (selection) {
const fromName = deriveCode(selection.name, typedQuery);
if (fromName) return fromName;
const fromLocation = deriveCode(selection.location, typedQuery);
if (fromLocation) return fromLocation;
}
return null;
}
function emitUpdate(selection: GeoSelection) {
dispatch('update', {
name: selection.name,
@@ -289,13 +322,15 @@
name: selectedStartLocation.name,
lat: selectedStartLocation.lat,
lng: selectedStartLocation.lng,
location: selectedStartLocation.location
location: selectedStartLocation.location,
code: startCode
},
end: {
name: selectedEndLocation.name,
lat: selectedEndLocation.lat,
lng: selectedEndLocation.lng,
location: selectedEndLocation.location
location: selectedEndLocation.location,
code: endCode
}
});
}
@@ -307,7 +342,7 @@
mapCenter = [searchResult.lng, searchResult.lat];
mapZoom = 14;
searchResults = [];
searchQuery = searchResult.name;
searchQuery = searchResult.location || searchResult.name;
displayName = searchResult.location || searchResult.name;
@@ -320,12 +355,23 @@
startMarker = { lng: searchResult.lng, lat: searchResult.lat };
startSearchResults = [];
// Extract airport code if in airport mode
const typedQuery = startSearchQuery;
// Only auto-derive and surface codes in airport mode
if (airportMode) {
const airportCodeMatch = searchResult.name.match(/\(([A-Z]{3})\)/);
startSearchQuery = airportCodeMatch ? airportCodeMatch[1] : searchResult.name;
startCode = resolveCode(searchResult, typedQuery);
if (!startCode) {
startCode =
deriveCode(searchResult.name, startSearchQuery) || deriveCode(searchResult.location);
}
if (startCode) {
startSearchQuery = startCode;
}
} else {
startSearchQuery = searchResult.name;
startSearchQuery = searchResult.location || searchResult.name;
startCode = null;
}
await performDetailedReverseGeocode(searchResult.lat, searchResult.lng, 'start');
@@ -338,12 +384,23 @@
endMarker = { lng: searchResult.lng, lat: searchResult.lat };
endSearchResults = [];
// Extract airport code if in airport mode
const typedQuery = endSearchQuery;
// Only auto-derive and surface codes in airport mode
if (airportMode) {
const airportCodeMatch = searchResult.name.match(/\(([A-Z]{3})\)/);
endSearchQuery = airportCodeMatch ? airportCodeMatch[1] : searchResult.name;
endCode = resolveCode(searchResult, typedQuery);
if (!endCode) {
endCode =
deriveCode(searchResult.name, endSearchQuery) || deriveCode(searchResult.location);
}
if (endCode) {
endSearchQuery = endCode;
}
} else {
endSearchQuery = searchResult.name;
endSearchQuery = searchResult.location || searchResult.name;
endCode = null;
}
await performDetailedReverseGeocode(searchResult.lat, searchResult.lng, 'end');
@@ -394,7 +451,7 @@
type: result.type,
category: result.category
};
searchQuery = result.name;
searchQuery = result.display_name || result.name;
displayName = result.display_name || result.name;
} else {
selectedLocation = {
@@ -522,6 +579,8 @@
endMarker = null;
startLocationData = null;
endLocationData = null;
startCode = null;
endCode = null;
startSearchQuery = '';
endSearchQuery = '';
startSearchResults = [];
@@ -736,6 +795,9 @@
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-base-content/80 truncate">
{selectedStartLocation.name}
{#if startCode}
<span class="badge badge-success badge-sm ml-2">{startCode}</span>
{/if}
</p>
<p class="text-xs text-base-content/60">
{startMarker?.lat.toFixed(6)}, {startMarker?.lng.toFixed(6)}
@@ -751,6 +813,9 @@
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-base-content/80 truncate">
{selectedEndLocation.name}
{#if endCode}
<span class="badge badge-error badge-sm ml-2">{endCode}</span>
{/if}
</p>
<p class="text-xs text-base-content/60">
{endMarker?.lat.toFixed(6)}, {endMarker?.lng.toFixed(6)}

View File

@@ -58,6 +58,8 @@
origin_longitude: null,
destination_latitude: null,
destination_longitude: null,
start_code: null,
end_code: null,
distance: null,
collection: collection?.id,
is_public: true
@@ -70,6 +72,8 @@
let constrainDates: boolean = true;
let fullStartDate: string = '';
let fullEndDate: string = '';
let startCodeField: string = '';
let endCodeField: string = '';
let user: User | null = null;
let transportationToEdit: Transportation | null = null;
@@ -86,6 +90,33 @@
transportation.end_timezone = allDay ? null : selectedTimezone;
}
function handleStartCodeInput(value: string) {
startCodeField = value;
transportation.start_code = normalizeCode(value);
}
function handleEndCodeInput(value: string) {
endCodeField = value;
transportation.end_code = normalizeCode(value);
}
function handleStartCodeEvent(event: Event) {
const target = event.target as HTMLInputElement;
handleStartCodeInput(target?.value || '');
}
function handleEndCodeEvent(event: Event) {
const target = event.target as HTMLInputElement;
handleEndCodeInput(target?.value || '');
}
function normalizeCode(code: string | null): string | null {
if (!code) return null;
const trimmed = code.trim().toUpperCase();
if (!trimmed) return null;
return trimmed.slice(0, 5);
}
// Reactive constraints
$: constraintStartDate = allDay
? fullStartDate && fullStartDate.includes('T')
@@ -100,8 +131,8 @@
function handleTransportationUpdate(
event: CustomEvent<{
start: { name: string; lat: number; lng: number; location: string };
end: { name: string; lat: number; lng: number; location: string };
start: { name: string; lat: number; lng: number; location: string; code?: string | null };
end: { name: string; lat: number; lng: number; location: string; code?: string | null };
}>
) {
const { start, end } = event.detail;
@@ -110,11 +141,15 @@
transportation.from_location = start.location;
transportation.origin_latitude = start.lat;
transportation.origin_longitude = start.lng;
transportation.start_code = normalizeCode(start.code || '');
startCodeField = startCodeField || transportation.start_code || '';
// Update to location
transportation.to_location = end.location;
transportation.destination_latitude = end.lat;
transportation.destination_longitude = end.lng;
transportation.end_code = normalizeCode(end.code || '');
endCodeField = endCodeField || transportation.end_code || '';
// Update name if empty (use route)
if (!transportation.name) {
@@ -129,6 +164,8 @@
transportation.origin_longitude = null;
transportation.destination_latitude = null;
transportation.destination_longitude = null;
transportation.start_code = null;
transportation.end_code = null;
}
function handleAllDayToggle() {
@@ -255,6 +292,10 @@
transportation.start_timezone = allDay ? null : selectedTimezone;
transportation.end_timezone = allDay ? null : selectedTimezone;
// Normalize codes before sending
transportation.start_code = normalizeCode(startCodeField || transportation.start_code);
transportation.end_code = normalizeCode(endCodeField || transportation.end_code);
if (!syncAndValidateDates(true)) {
return;
}
@@ -389,6 +430,8 @@
transportation.rating = initialTransportation.rating ?? NaN;
transportation.is_public = initialTransportation.is_public ?? true;
transportation.flight_number = initialTransportation.flight_number || null;
transportation.start_code = initialTransportation.start_code || null;
transportation.end_code = initialTransportation.end_code || null;
transportation.distance = initialTransportation.distance || null;
// Populate origin/destination data
@@ -398,6 +441,8 @@
transportation.origin_longitude = initialTransportation.origin_longitude || null;
transportation.destination_latitude = initialTransportation.destination_latitude || null;
transportation.destination_longitude = initialTransportation.destination_longitude || null;
startCodeField = transportation.start_code || '';
endCodeField = transportation.end_code || '';
if (initialTransportation.user) {
ownerUser = initialTransportation.user;
@@ -460,6 +505,8 @@
location: initialTransportation.to_location || ''
}
: null}
initialStartCode={initialTransportation?.start_code || null}
initialEndCode={initialTransportation?.end_code || null}
on:transportationUpdate={handleTransportationUpdate}
on:clear={handleLocationClear}
/>
@@ -531,6 +578,50 @@
/>
</div>
<!-- Start/End Codes -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div class="form-control">
<label class="label" for="start_code">
<span class="label-text font-medium"
>{$t('transportation.departure_code') || 'Departure code'}</span
>
</label>
<input
type="text"
id="start_code"
value={startCodeField}
on:input={handleStartCodeEvent}
class="input input-bordered bg-base-100/80 focus:bg-base-100 uppercase"
maxlength="5"
placeholder={airportMode ? 'JFK' : 'Code'}
/>
<p class="text-xs text-base-content/60 mt-1">
{$t('transportation.autofill_code_hint') ||
'Auto-filled from airport search; you can override'}
</p>
</div>
<div class="form-control">
<label class="label" for="end_code">
<span class="label-text font-medium"
>{$t('transportation.arrival_code') || 'Arrival code'}</span
>
</label>
<input
type="text"
id="end_code"
value={endCodeField}
on:input={handleEndCodeEvent}
class="input input-bordered bg-base-100/80 focus:bg-base-100 uppercase"
maxlength="5"
placeholder={airportMode ? 'LHR' : 'Code'}
/>
<p class="text-xs text-base-content/60 mt-1">
{$t('transportation.autofill_code_hint_arrival') ||
'Auto-filled from arrival search; you can override'}
</p>
</div>
</div>
<!-- Rating Field -->
<div class="form-control">
<label class="label" for="rating">

View File

@@ -51,6 +51,8 @@
origin_longitude: null,
destination_latitude: null,
destination_longitude: null,
start_code: null,
end_code: null,
is_public: false,
distance: null,
collection: null,
@@ -97,6 +99,8 @@
origin_longitude: transportationToEdit.origin_longitude || null,
destination_latitude: transportationToEdit.destination_latitude || null,
destination_longitude: transportationToEdit.destination_longitude || null,
start_code: transportationToEdit.start_code || null,
end_code: transportationToEdit.end_code || null,
is_public: transportationToEdit.is_public || false,
distance: transportationToEdit.distance || null,
collection: transportationToEdit.collection || null,

View File

@@ -189,6 +189,8 @@ export type Transportation = {
origin_longitude: number | null;
destination_latitude: number | null;
destination_longitude: number | null;
start_code: string | null; // Could be airport code, station code, etc.
end_code: string | null; // Could be airport code, station code, etc.
is_public: boolean;
distance: number | null; // in kilometers
collection: Collection | null | string;

View File

@@ -490,7 +490,9 @@
"lodging_not_found": "Lodging Not Found",
"stay_dates": "Stay Dates",
"nights": "Nights",
"reservation": "Reservation"
"reservation": "Reservation",
"flight": "Flight",
"route": "Route"
},
"worldtravel": {
"country_list": "Country List",
@@ -764,7 +766,11 @@
"archived_appear_here": "Archived collections will appear here.",
"linked": "Linked",
"available": "Available",
"try_different_search": "Try a different search or filter."
"try_different_search": "Try a different search or filter.",
"update_collection_details": "Updte collection details",
"create_new_collection": "Create new collection",
"public_collection_description": "Allow anyone with the link to view",
"enter_collection_name": "Enter collection name"
},
"notes": {
"note_deleted": "Note deleted successfully!",

View File

@@ -19,10 +19,8 @@
import StarOutline from '~icons/mdi/star-outline';
import MapMarker from '~icons/mdi/map-marker';
import CalendarRange from '~icons/mdi/calendar-range';
import Eye from '~icons/mdi/eye';
import EyeOff from '~icons/mdi/eye-off';
import OpenInNew from '~icons/mdi/open-in-new';
import CashMultiple from '~icons/mdi/cash-multiple';
import MapMarkerDistanceIcon from '~icons/mdi/map-marker-distance';
import CardAccountDetails from '~icons/mdi/card-account-details';
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
import TransportationModal from '$lib/components/transportation/TransportationModal.svelte';
@@ -169,6 +167,13 @@
}
return null;
}
function getRouteCodes(item: Transportation): string | null {
if (item.start_code && item.end_code) return `${item.start_code}${item.end_code}`;
if (item.start_code) return item.start_code;
if (item.end_code) return item.end_code;
return null;
}
</script>
{#if notFound}
@@ -297,6 +302,11 @@
🏁 {transportation.to_location}
</div>
{/if}
{#if getRouteCodes(transportation)}
<div class="badge badge-lg badge-outline font-semibold px-4 py-3 gap-2">
✈️ {getRouteCodes(transportation)}
</div>
{/if}
{#if transportation.is_public}
<div class="badge badge-lg badge-accent font-semibold px-4 py-3">
👁️ {$t('adventures.public')}
@@ -549,10 +559,23 @@
</div>
{/if}
<!-- Route Codes -->
{#if getRouteCodes(transportation)}
<div class="flex items-start gap-3">
<MapMarker class="w-5 h-5 text-primary mt-1 flex-shrink-0" />
<div>
<p class="font-semibold text-sm opacity-70">
{$t('transportation.codes') ?? 'Codes'}
</p>
<p class="text-base font-mono">{getRouteCodes(transportation)}</p>
</div>
</div>
{/if}
<!-- Distance -->
{#if transportation.distance}
<div class="flex items-start gap-3">
<CashMultiple class="w-5 h-5 text-primary mt-1 flex-shrink-0" />
<MapMarkerDistanceIcon class="w-5 h-5 text-primary mt-1 flex-shrink-0" />
<div>
<p class="font-semibold text-sm opacity-70">
{$t('adventures.distance') ?? 'Distance'}