mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-05-08 23:15:11 -04:00
feat: add start_code and end_code fields to Transportation model and update related components
This commit is contained in:
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user