final refactoring
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,3 +23,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
> map-journal@0.0.0 dev
|
|
||||||
> vite
|
|
||||||
|
|
||||||
Port 5173 is in use, trying another one...
|
|
||||||
Port 5174 is in use, trying another one...
|
|
||||||
|
|
||||||
[32m[1mVITE[22m v8.0.15[39m [2mready in [0m[1m1792[22m[2m[0m ms[22m
|
|
||||||
|
|
||||||
[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m5175[22m/[39m
|
|
||||||
[2m [32m➜[39m [1mNetwork[22m[2m: use [22m[1m--host[22m[2m to expose[22m
|
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -10,7 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"firebase": "^12.14.0",
|
"firebase": "^12.14.0",
|
||||||
"flag-icons": "^7.5.0",
|
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "^3.1.0",
|
||||||
"world-atlas": "^2.0.2"
|
"world-atlas": "^2.0.2"
|
||||||
@@ -2336,12 +2335,6 @@
|
|||||||
"@firebase/util": "1.15.1"
|
"@firebase/util": "1.15.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flag-icons": {
|
|
||||||
"version": "7.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz",
|
|
||||||
"integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"firebase": "^12.14.0",
|
"firebase": "^12.14.0",
|
||||||
"flag-icons": "^7.5.0",
|
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "^3.1.0",
|
||||||
"world-atlas": "^2.0.2"
|
"world-atlas": "^2.0.2"
|
||||||
|
|||||||
@@ -117,12 +117,12 @@
|
|||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
padding: 12px 28px;
|
padding: 10px 22px;
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
border: none;
|
border: none;
|
||||||
background: #8b5cf6;
|
background: #8b5cf6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 290 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB |
@@ -12,7 +12,7 @@ const firebaseConfig = {
|
|||||||
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const app = initializeApp(firebaseConfig);
|
const app = initializeApp(firebaseConfig);
|
||||||
export const auth = getAuth(app);
|
export const auth = getAuth(app);
|
||||||
export const db = getFirestore(app);
|
export const db = getFirestore(app);
|
||||||
export const storage = getStorage(app);
|
export const storage = getStorage(app);
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { getSelected, getTotalCount } from './selection.svelte.js';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<span>{getSelected().size} / {getTotalCount()} countries visited</span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.footer {
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 300;
|
|
||||||
color: var(--text-sub);
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
background: var(--bg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { journals } from '../stores/journalStore.js';
|
import { journals } from '../stores/entriesStore.svelte.js';
|
||||||
import { nameToId } from '../shared/countries.js';
|
import { nameToId } from '../shared/countries.js';
|
||||||
import { getUserProfile } from '../auth/userStore.svelte.js';
|
import { getUserProfile } from '../auth/userStore.svelte.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
<script>
|
|
||||||
/**
|
|
||||||
* Reusable photo gallery with prev/next arrows and indicator.
|
|
||||||
* @type {{
|
|
||||||
* photos: string[],
|
|
||||||
* height?: string,
|
|
||||||
* thumbs?: boolean,
|
|
||||||
* counter?: boolean,
|
|
||||||
* onStep?: (e: Event) => void,
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
let { photos, height = '220px', thumbs = false, counter = false } = $props();
|
|
||||||
|
|
||||||
let idx = $state(0);
|
|
||||||
|
|
||||||
function prev(e) {
|
|
||||||
e?.stopPropagation();
|
|
||||||
idx = (idx - 1 + photos.length) % photos.length;
|
|
||||||
}
|
|
||||||
function next(e) {
|
|
||||||
e?.stopPropagation();
|
|
||||||
idx = (idx + 1) % photos.length;
|
|
||||||
}
|
|
||||||
function go(i, e) {
|
|
||||||
e?.stopPropagation();
|
|
||||||
idx = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset when photos change (e.g. navigating to a different entry)
|
|
||||||
$effect(() => {
|
|
||||||
photos;
|
|
||||||
idx = 0;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if photos.length > 0}
|
|
||||||
<div class="gallery" style="--gallery-height: {height}">
|
|
||||||
<img class="gallery-img" src={photos[idx]} alt="photo {idx + 1}" loading="lazy" />
|
|
||||||
|
|
||||||
{#if photos.length > 1}
|
|
||||||
<button class="arr left" onclick={prev} aria-label="Previous photo">‹</button>
|
|
||||||
<button class="arr right" onclick={next} aria-label="Next photo">›</button>
|
|
||||||
|
|
||||||
{#if thumbs}
|
|
||||||
<div class="thumb-strip">
|
|
||||||
{#each photos as photo, i}
|
|
||||||
<button class="thumb" class:active={i === idx} onclick={(e) => go(i, e)} aria-label="Photo {i + 1}">
|
|
||||||
<img src={photo} alt="" />
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="dots">
|
|
||||||
{#each photos as _, i}
|
|
||||||
<button class="pip" class:active={i === idx} onclick={(e) => go(i, e)} aria-label="Photo {i + 1}"></button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if counter}
|
|
||||||
<span class="counter">{idx + 1} / {photos.length}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.gallery {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-img {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--gallery-height);
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arr {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: rgba(0,0,0,0.45);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background 0.15s;
|
|
||||||
z-index: 2;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
.arr:hover { background: rgba(0,0,0,0.7); }
|
|
||||||
.arr.left { left: 10px; }
|
|
||||||
.arr.right { right: 10px; }
|
|
||||||
|
|
||||||
/* Dot indicators (timeline cards) */
|
|
||||||
.dots {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 8px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.pip {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: rgba(255,255,255,0.5);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
transition: background 0.15s, transform 0.15s;
|
|
||||||
}
|
|
||||||
.pip.active { background: #fff; transform: scale(1.3); }
|
|
||||||
|
|
||||||
/* Thumbnail strip (detail page) */
|
|
||||||
.thumb-strip {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 12px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.thumb {
|
|
||||||
width: 52px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.65;
|
|
||||||
background: none;
|
|
||||||
transition: border-color 0.15s, opacity 0.15s;
|
|
||||||
}
|
|
||||||
.thumb.active { border-color: #fff; opacity: 1; }
|
|
||||||
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
||||||
|
|
||||||
/* Photo counter badge */
|
|
||||||
.counter {
|
|
||||||
position: absolute;
|
|
||||||
top: 14px;
|
|
||||||
right: 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 300;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(0,0,0,0.45);
|
|
||||||
padding: 3px 10px;
|
|
||||||
border-radius: 20px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.thumb { width: 40px; height: 28px; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
export const countryCities = {
|
|
||||||
'Afghanistan': ['Kabul','Kandahar','Herat','Mazar-i-Sharif','Jalalabad'],
|
|
||||||
'Albania': ['Tirana','Durrës','Vlorë','Shkodër','Elbasan'],
|
|
||||||
'Algeria': ['Algiers','Oran','Constantine','Annaba','Blida'],
|
|
||||||
'Andorra': ['Andorra la Vella','Escaldes-Engordany','Encamp'],
|
|
||||||
'Angola': ['Luanda','Huambo','Lobito','Benguela','Lubango'],
|
|
||||||
'Argentina': ['Buenos Aires','Córdoba','Rosario','Mendoza','Bariloche','Salta','Mar del Plata'],
|
|
||||||
'Armenia': ['Yerevan','Gyumri','Vanadzor'],
|
|
||||||
'Australia': ['Sydney','Melbourne','Brisbane','Perth','Adelaide','Gold Coast','Cairns','Darwin','Hobart','Canberra'],
|
|
||||||
'Austria': ['Vienna','Salzburg','Graz','Innsbruck','Linz','Hallstatt'],
|
|
||||||
'Azerbaijan': ['Baku','Ganja','Sumqayit'],
|
|
||||||
'Bahamas': ['Nassau','Freeport'],
|
|
||||||
'Bahrain': ['Manama','Riffa','Muharraq'],
|
|
||||||
'Bangladesh': ['Dhaka','Chittagong','Sylhet','Rajshahi','Khulna'],
|
|
||||||
'Barbados': ['Bridgetown'],
|
|
||||||
'Belarus': ['Minsk','Gomel','Brest','Grodno'],
|
|
||||||
'Belgium': ['Brussels','Bruges','Ghent','Antwerp','Liège'],
|
|
||||||
'Belize': ['Belize City','San Ignacio','Placencia'],
|
|
||||||
'Benin': ['Cotonou','Porto-Novo','Abomey'],
|
|
||||||
'Bhutan': ['Thimphu','Paro','Punakha'],
|
|
||||||
'Bolivia': ['La Paz','Santa Cruz','Cochabamba','Sucre','Uyuni'],
|
|
||||||
'Bosnia and Herz.': ['Sarajevo','Mostar','Banja Luka'],
|
|
||||||
'Botswana': ['Gaborone','Francistown','Maun'],
|
|
||||||
'Brazil': ['São Paulo','Rio de Janeiro','Brasília','Salvador','Fortaleza','Manaus','Recife','Florianópolis','Foz do Iguaçu'],
|
|
||||||
'Brunei': ['Bandar Seri Begawan'],
|
|
||||||
'Bulgaria': ['Sofia','Plovdiv','Varna','Burgas','Ruse'],
|
|
||||||
'Burkina Faso': ['Ouagadougou','Bobo-Dioulasso'],
|
|
||||||
'Burundi': ['Bujumbura','Gitega'],
|
|
||||||
'Cabo Verde': ['Praia','Mindelo'],
|
|
||||||
'Cambodia': ['Phnom Penh','Siem Reap','Sihanoukville','Battambang'],
|
|
||||||
'Cameroon': ['Yaoundé','Douala','Bafoussam'],
|
|
||||||
'Canada': ['Toronto','Vancouver','Montreal','Calgary','Ottawa','Edmonton','Quebec City','Whistler','Banff','Niagara Falls','Halifax'],
|
|
||||||
'Central African Rep.': ['Bangui'],
|
|
||||||
'Chad': ["N'Djamena",'Moundou'],
|
|
||||||
'Chile': ['Santiago','Valparaíso','Atacama','Puerto Natales','Punta Arenas','Viña del Mar','San Pedro de Atacama'],
|
|
||||||
'China': ['Beijing','Shanghai','Guangzhou','Shenzhen','Chengdu','Xi\'an','Hangzhou','Chongqing','Guilin','Zhangjiajie','Lijiang','Hong Kong','Macau'],
|
|
||||||
'Colombia': ['Bogotá','Medellín','Cartagena','Cali','Santa Marta','Barranquilla'],
|
|
||||||
'Comoros': ['Moroni'],
|
|
||||||
'Congo': ['Brazzaville','Pointe-Noire'],
|
|
||||||
'Costa Rica': ['San José','Manuel Antonio','Tamarindo','Arenal','Monteverde'],
|
|
||||||
'Croatia': ['Zagreb','Dubrovnik','Split','Hvar','Zadar','Pula','Rijeka'],
|
|
||||||
'Cuba': ['Havana','Trinidad','Varadero','Santiago de Cuba','Cienfuegos'],
|
|
||||||
'Cyprus': ['Nicosia','Limassol','Paphos','Larnaca','Ayia Napa'],
|
|
||||||
'Czechia': ['Prague','Brno','Český Krumlov','Karlovy Vary','Olomouc'],
|
|
||||||
"Côte d'Ivoire": ['Abidjan','Yamoussoukro','Bouaké'],
|
|
||||||
'Dem. Rep. Congo': ['Kinshasa','Lubumbashi','Goma','Kisangani'],
|
|
||||||
'Denmark': ['Copenhagen','Aarhus','Odense','Aalborg'],
|
|
||||||
'Djibouti': ['Djibouti City'],
|
|
||||||
'Dominican Rep.': ['Santo Domingo','Punta Cana','Santiago','La Romana'],
|
|
||||||
'Ecuador': ['Quito','Guayaquil','Cuenca','Baños','Galápagos Islands'],
|
|
||||||
'Egypt': ['Cairo','Alexandria','Luxor','Aswan','Sharm el-Sheikh','Hurghada','Giza'],
|
|
||||||
'El Salvador': ['San Salvador','Santa Ana','San Miguel'],
|
|
||||||
'Eq. Guinea': ['Malabo','Bata'],
|
|
||||||
'Eritrea': ['Asmara','Massawa'],
|
|
||||||
'Estonia': ['Tallinn','Tartu','Pärnu'],
|
|
||||||
'Ethiopia': ['Addis Ababa','Lalibela','Gondar','Axum','Dire Dawa'],
|
|
||||||
'Fiji': ['Suva','Nadi','Mamanuca Islands'],
|
|
||||||
'Finland': ['Helsinki','Rovaniemi','Tampere','Turku','Oulu'],
|
|
||||||
'Fr. Polynesia': ['Papeete','Bora Bora','Moorea'],
|
|
||||||
'France': ['Paris','Nice','Lyon','Marseille','Bordeaux','Strasbourg','Toulouse','Cannes','Monaco','Mont Saint-Michel','Versailles'],
|
|
||||||
'Gabon': ['Libreville','Port-Gentil'],
|
|
||||||
'Gambia': ['Banjul','Serekunda'],
|
|
||||||
'Georgia': ['Tbilisi','Batumi','Kutaisi','Sighnaghi'],
|
|
||||||
'Germany': ['Berlin','Munich','Hamburg','Frankfurt','Cologne','Dresden','Heidelberg','Rothenburg ob der Tauber','Neuschwanstein','Stuttgart'],
|
|
||||||
'Ghana': ['Accra','Kumasi','Cape Coast','Tamale'],
|
|
||||||
'Greece': ['Athens','Santorini','Mykonos','Rhodes','Thessaloniki','Crete','Corfu','Meteora'],
|
|
||||||
'Greenland': ['Nuuk','Ilulissat'],
|
|
||||||
'Grenada': ["St. George's"],
|
|
||||||
'Guatemala': ['Guatemala City','Antigua','Lake Atitlán','Tikal','Quetzaltenango'],
|
|
||||||
'Guinea': ['Conakry'],
|
|
||||||
'Guyana': ['Georgetown'],
|
|
||||||
'Haiti': ['Port-au-Prince','Cap-Haïtien'],
|
|
||||||
'Honduras': ['Tegucigalpa','San Pedro Sula','Roatán'],
|
|
||||||
'Hong Kong': ['Hong Kong'],
|
|
||||||
'Hungary': ['Budapest','Debrecen','Pécs','Eger','Győr'],
|
|
||||||
'Iceland': ['Reykjavik','Akureyri','Blue Lagoon','Golden Circle'],
|
|
||||||
'India': ['Mumbai','Delhi','Jaipur','Agra','Bangalore','Chennai','Kolkata','Goa','Varanasi','Udaipur','Kerala','Leh','Shimla'],
|
|
||||||
'Indonesia': ['Jakarta','Bali','Yogyakarta','Lombok','Medan','Komodo','Raja Ampat','Surabaya'],
|
|
||||||
'Iran': ['Tehran','Isfahan','Shiraz','Persepolis','Yazd'],
|
|
||||||
'Iraq': ['Baghdad','Erbil','Basra','Najaf'],
|
|
||||||
'Ireland': ['Dublin','Cork','Galway','Killarney','Limerick'],
|
|
||||||
'Israel': ['Jerusalem','Tel Aviv','Haifa','Eilat','Dead Sea'],
|
|
||||||
'Italy': ['Rome','Florence','Venice','Milan','Naples','Amalfi','Sicily','Cinque Terre','Bologna','Turin'],
|
|
||||||
'Jamaica': ['Kingston','Montego Bay','Negril','Ocho Rios'],
|
|
||||||
'Japan': ['Tokyo','Kyoto','Osaka','Hiroshima','Nara','Sapporo','Hakone','Nikko','Kanazawa','Okinawa','Fukuoka'],
|
|
||||||
'Jordan': ['Amman','Petra','Wadi Rum','Aqaba','Jerash'],
|
|
||||||
'Kazakhstan': ['Almaty','Nur-Sultan','Shymkent'],
|
|
||||||
'Kenya': ['Nairobi','Mombasa','Masai Mara','Amboseli','Zanzibar'],
|
|
||||||
'Kosovo': ['Pristina','Prizren'],
|
|
||||||
'Kuwait': ['Kuwait City'],
|
|
||||||
'Kyrgyzstan': ['Bishkek','Osh','Karakol'],
|
|
||||||
'Laos': ['Vientiane','Luang Prabang','Vang Vieng'],
|
|
||||||
'Latvia': ['Riga','Jūrmala','Sigulda'],
|
|
||||||
'Lebanon': ['Beirut','Byblos','Baalbek','Sidon'],
|
|
||||||
'Libya': ['Tripoli','Benghazi','Leptis Magna'],
|
|
||||||
'Liechtenstein': ['Vaduz'],
|
|
||||||
'Lithuania': ['Vilnius','Kaunas','Trakai','Klaipėda'],
|
|
||||||
'Luxembourg': ['Luxembourg City','Vianden'],
|
|
||||||
'Madagascar': ['Antananarivo','Nosy Be','Morondava'],
|
|
||||||
'Malawi': ['Lilongwe','Blantyre','Lake Malawi'],
|
|
||||||
'Malaysia': ['Kuala Lumpur','Penang','Langkawi','Kota Kinabalu','Malacca','George Town'],
|
|
||||||
'Maldives': ['Malé','Maafushi'],
|
|
||||||
'Mali': ['Bamako','Timbuktu','Djenné'],
|
|
||||||
'Malta': ['Valletta','Mdina','Gozo'],
|
|
||||||
'Mauritania': ['Nouakchott'],
|
|
||||||
'Mauritius': ['Port Louis','Grand Baie','Flic en Flac'],
|
|
||||||
'Mexico': ['Mexico City','Cancún','Guadalajara','Oaxaca','Tulum','Playa del Carmen','San Miguel de Allende','Monterrey','Chichen Itza'],
|
|
||||||
'Moldova': ['Chișinău'],
|
|
||||||
'Monaco': ['Monaco'],
|
|
||||||
'Mongolia': ['Ulaanbaatar','Gobi Desert'],
|
|
||||||
'Montenegro': ['Podgorica','Kotor','Budva','Bar'],
|
|
||||||
'Morocco': ['Marrakech','Fes','Casablanca','Rabat','Chefchaouen','Essaouira','Sahara Desert'],
|
|
||||||
'Mozambique': ['Maputo','Beira','Pemba'],
|
|
||||||
'Myanmar': ['Yangon','Bagan','Mandalay','Inle Lake'],
|
|
||||||
'Namibia': ['Windhoek','Swakopmund','Etosha','Sossusvlei'],
|
|
||||||
'Nepal': ['Kathmandu','Pokhara','Everest Base Camp','Chitwan','Lumbini'],
|
|
||||||
'Netherlands': ['Amsterdam','Rotterdam','The Hague','Utrecht','Delft','Eindhoven'],
|
|
||||||
'New Zealand': ['Auckland','Queenstown','Wellington','Christchurch','Rotorua','Milford Sound'],
|
|
||||||
'Nicaragua': ['Managua','Granada','León'],
|
|
||||||
'Niger': ['Niamey','Agadez'],
|
|
||||||
'Nigeria': ['Lagos','Abuja','Kano','Ibadan'],
|
|
||||||
'North Korea': ['Pyongyang'],
|
|
||||||
'North Macedonia': ['Skopje','Ohrid'],
|
|
||||||
'Norway': ['Oslo','Bergen','Tromsø','Flåm','Ålesund','Stavanger'],
|
|
||||||
'Oman': ['Muscat','Nizwa','Salalah','Wahiba Sands'],
|
|
||||||
'Pakistan': ['Karachi','Lahore','Islamabad','Peshawar','Gilgit'],
|
|
||||||
'Palestine': ['Ramallah','Bethlehem','Jericho','Hebron'],
|
|
||||||
'Panama': ['Panama City','Bocas del Toro','Boquete'],
|
|
||||||
'Papua New Guinea': ['Port Moresby'],
|
|
||||||
'Paraguay': ['Asunción','Ciudad del Este'],
|
|
||||||
'Peru': ['Lima','Cusco','Machu Picchu','Arequipa','Puno','Iquitos'],
|
|
||||||
'Philippines': ['Manila','Cebu','Palawan','Boracay','Davao','Siargao'],
|
|
||||||
'Poland': ['Warsaw','Kraków','Gdańsk','Wrocław','Poznań','Zakopane'],
|
|
||||||
'Portugal': ['Lisbon','Porto','Algarve','Sintra','Madeira','Azores','Évora'],
|
|
||||||
'Puerto Rico': ['San Juan','Ponce','Rincon'],
|
|
||||||
'Qatar': ['Doha'],
|
|
||||||
'Romania': ['Bucharest','Transylvania','Cluj-Napoca','Sibiu','Brașov','Sinaia'],
|
|
||||||
'Russia': ['Moscow','St. Petersburg','Irkutsk','Vladivostok','Sochi','Kazan','Novosibirsk'],
|
|
||||||
'Rwanda': ['Kigali','Volcanoes National Park'],
|
|
||||||
'S. Sudan': ['Juba'],
|
|
||||||
'Saint Lucia': ['Castries','Soufrière'],
|
|
||||||
'Saudi Arabia': ['Riyadh','Jeddah','Mecca','Medina','AlUla','NEOM'],
|
|
||||||
'Senegal': ['Dakar','Saint-Louis','Ziguinchor'],
|
|
||||||
'Serbia': ['Belgrade','Novi Sad','Niš'],
|
|
||||||
'Seychelles': ['Victoria','La Digue','Praslin','Mahé'],
|
|
||||||
'Sierra Leone': ['Freetown'],
|
|
||||||
'Singapore': ['Singapore'],
|
|
||||||
'Slovakia': ['Bratislava','Košice','Banská Bystrica'],
|
|
||||||
'Slovenia': ['Ljubljana','Bled','Piran','Maribor'],
|
|
||||||
'Solomon Is.': ['Honiara'],
|
|
||||||
'Somalia': ['Mogadishu'],
|
|
||||||
'South Africa': ['Cape Town','Johannesburg','Durban','Stellenbosch','Kruger','Garden Route','Pretoria'],
|
|
||||||
'South Korea': ['Seoul','Busan','Jeju','Gyeongju','Incheon','Suwon'],
|
|
||||||
'Spain': ['Barcelona','Madrid','Seville','Granada','Valencia','Bilbao','Toledo','San Sebastián','Ibiza','Mallorca'],
|
|
||||||
'Sri Lanka': ['Colombo','Kandy','Galle','Ella','Sigiriya','Mirissa'],
|
|
||||||
'Sudan': ['Khartoum','Omdurman'],
|
|
||||||
'Suriname': ['Paramaribo'],
|
|
||||||
'Sweden': ['Stockholm','Gothenburg','Malmö','Uppsala','Kiruna'],
|
|
||||||
'Switzerland': ['Zurich','Geneva','Bern','Interlaken','Lucerne','Zermatt','Lugano','Grindelwald'],
|
|
||||||
'Syria': ['Damascus','Aleppo','Palmyra'],
|
|
||||||
'São Tomé and Príncipe': ['São Tomé'],
|
|
||||||
'Taiwan': ['Taipei','Kaohsiung','Tainan','Taichung'],
|
|
||||||
'Tajikistan': ['Dushanbe','Khujand'],
|
|
||||||
'Tanzania': ['Dar es Salaam','Zanzibar','Serengeti','Arusha','Kilimanjaro'],
|
|
||||||
'Thailand': ['Bangkok','Chiang Mai','Phuket','Koh Samui','Koh Phi Phi','Ayutthaya','Pai','Krabi'],
|
|
||||||
'Timor-Leste': ['Dili'],
|
|
||||||
'Togo': ['Lomé'],
|
|
||||||
'Trinidad and Tobago': ['Port of Spain'],
|
|
||||||
'Tunisia': ['Tunis','Carthage','Sousse','Hammamet','Djerba'],
|
|
||||||
'Turkey': ['Istanbul','Cappadocia','Antalya','Bodrum','Ankara','Ephesus','Pamukkale','Trabzon'],
|
|
||||||
'Turkmenistan': ['Ashgabat','Merv'],
|
|
||||||
'Uganda': ['Kampala','Bwindi','Jinja'],
|
|
||||||
'Ukraine': ['Kyiv','Lviv','Odessa','Kharkiv'],
|
|
||||||
'United Arab Emirates': ['Dubai','Abu Dhabi','Sharjah'],
|
|
||||||
'United Kingdom': ['London','Edinburgh','Manchester','Liverpool','Oxford','Cambridge','Bath','York','Brighton','Glasgow','Dublin'],
|
|
||||||
'United States of America': ['New York','Los Angeles','Chicago','Miami','San Francisco','Las Vegas','New Orleans','Seattle','Boston','Washington D.C.','Nashville','Denver','Honolulu','Anchorage','Portland'],
|
|
||||||
'Uruguay': ['Montevideo','Punta del Este','Colonia del Sacramento'],
|
|
||||||
'Uzbekistan': ['Tashkent','Samarkand','Bukhara','Khiva'],
|
|
||||||
'Venezuela': ['Caracas','Medellín','Canaima','Los Roques'],
|
|
||||||
'Vietnam': ['Hanoi','Ho Chi Minh City','Hoi An','Da Nang','Ha Long Bay','Hue','Sapa','Phu Quoc'],
|
|
||||||
'Yemen': ["Sana'a",'Aden'],
|
|
||||||
'Zambia': ['Lusaka','Livingstone','Victoria Falls'],
|
|
||||||
'Zimbabwe': ['Harare','Bulawayo','Victoria Falls'],
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { journals } from './entriesStore.svelte.js';
|
|
||||||
export { addEntry as addJournal } from './entriesStore.svelte.js';
|
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getEntries } from '../../stores/entriesStore.svelte.js';
|
|
||||||
import { addEntry, updateEntry } from '../../stores/entriesStore.svelte.js';
|
import { addEntry, updateEntry } from '../../stores/entriesStore.svelte.js';
|
||||||
import { countryNames } from '../../shared/countries.js';
|
|
||||||
import { getCitiesForCountry, ALL_CITIES } from '../../shared/cities.js';
|
|
||||||
import SearchInput from '../../shared/SearchInput.svelte';
|
|
||||||
import PhotoEditor from './PhotoEditor.svelte';
|
import PhotoEditor from './PhotoEditor.svelte';
|
||||||
import airplaneImg from '../../../assets/airplane.png';
|
import StepNav from './StepNavbar.svelte';
|
||||||
import trainImg from '../../../assets/train.png';
|
import TripBasicInfo from './TripBasicInfo.svelte';
|
||||||
import busImg from '../../../assets/bus.png';
|
|
||||||
import carImg from '../../../assets/car.png';
|
|
||||||
import shipImg from '../../../assets/ship.png';
|
|
||||||
import walkImg from '../../../assets/walk.png';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* entry = null → "new entry" mode
|
* entry = null → "new entry" mode
|
||||||
@@ -22,7 +14,6 @@
|
|||||||
let isNew = !entry;
|
let isNew = !entry;
|
||||||
|
|
||||||
let cities = $state([...(entry?.location.cities ?? [])]);
|
let cities = $state([...(entry?.location.cities ?? [])]);
|
||||||
let cityInput = $state('');
|
|
||||||
let country = $state(entry?.location.country ?? initialCountry);
|
let country = $state(entry?.location.country ?? initialCountry);
|
||||||
let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10));
|
let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10));
|
||||||
let days = $state(String(entry?.days ?? ''));
|
let days = $state(String(entry?.days ?? ''));
|
||||||
@@ -31,7 +22,7 @@
|
|||||||
let memo = $state(entry?.memo ?? '');
|
let memo = $state(entry?.memo ?? '');
|
||||||
let transport = $state(entry?.transport ?? '');
|
let transport = $state(entry?.transport ?? '');
|
||||||
|
|
||||||
let step = $state(1); // 1 | 2 | 3
|
let step = $state(1);
|
||||||
|
|
||||||
let errors = $state({
|
let errors = $state({
|
||||||
country: '', cities: '', date: '', days: '', tripType: '', transport: ''
|
country: '', cities: '', date: '', days: '', tripType: '', transport: ''
|
||||||
@@ -41,15 +32,6 @@
|
|||||||
errors = { country: '', cities: '', date: '', days: '', tripType: '', transport: '' };
|
errors = { country: '', cities: '', date: '', days: '', tripType: '', transport: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const transportOptions = [
|
|
||||||
{ value: 'flight', label: 'Flight', img: airplaneImg },
|
|
||||||
{ value: 'train', label: 'Train', img: trainImg },
|
|
||||||
{ value: 'bus', label: 'Bus', img: busImg },
|
|
||||||
{ value: 'car', label: 'Car', img: carImg },
|
|
||||||
{ value: 'ship', label: 'Ship', img: shipImg },
|
|
||||||
{ value: 'walk', label: 'Walk', img: walkImg },
|
|
||||||
];
|
|
||||||
|
|
||||||
const MEMO_MAX = 100;
|
const MEMO_MAX = 100;
|
||||||
let wordCount = $derived(memo.trim() === '' ? 0 : memo.trim().split(/\s+/).length);
|
let wordCount = $derived(memo.trim() === '' ? 0 : memo.trim().split(/\s+/).length);
|
||||||
let memoOverLimit = $derived(wordCount > MEMO_MAX);
|
let memoOverLimit = $derived(wordCount > MEMO_MAX);
|
||||||
@@ -65,25 +47,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let allEntries = $derived(getEntries());
|
|
||||||
let cityOptions = $derived(
|
|
||||||
country.trim()
|
|
||||||
? [...new Set([...getCitiesForCountry(country), ...allEntries.filter(j => (j.location.country || '').toLowerCase() === country.trim().toLowerCase()).flatMap(e => e.location.cities)])].sort()
|
|
||||||
: [...new Set([...Object.values(ALL_CITIES).flat(), ...allEntries.flatMap(e => e.location.cities)])].sort()
|
|
||||||
);
|
|
||||||
|
|
||||||
function addCity(val) {
|
|
||||||
const trimmed = (val ?? cityInput).trim();
|
|
||||||
if (trimmed && !cities.includes(trimmed)) {
|
|
||||||
cities = [...cities, trimmed];
|
|
||||||
}
|
|
||||||
cityInput = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCity(c) {
|
|
||||||
cities = cities.filter(x => x !== c);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextStep() {
|
function nextStep() {
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
@@ -133,100 +96,22 @@
|
|||||||
console.error('Save failed:', err);
|
console.error('Save failed:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let next = $derived(step < 3 ? nextStep : save);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
<StepNav {step} totalSteps={3} onback={prevStep} onnext={next} />
|
||||||
<header class="topbar">
|
|
||||||
<div class="topbar-left">
|
|
||||||
<button class="ghost-btn" onclick={prevStep}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
|
||||||
{step === 1 ? 'Back' : 'Previous'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="steps">
|
|
||||||
{#each [1,2,3] as s}
|
|
||||||
<div class="step-dot" class:active={step === s} class:done={step > s}></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="topbar-right">
|
|
||||||
{#if step < 3}
|
|
||||||
<button class="save-btn" onclick={nextStep}>Next</button>
|
|
||||||
{:else}
|
|
||||||
<button class="save-btn" onclick={save}>Save changes</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
|
|
||||||
{#if step === 1}
|
{#if step === 1}
|
||||||
<h1 class="page-headline">
|
<TripBasicInfo
|
||||||
{isNew ? 'Journal your trip!' : 'Edit your trip'}
|
bind:country bind:cities
|
||||||
</h1>
|
bind:date bind:days bind:tripType bind:transport
|
||||||
|
bind:errors {isNew}
|
||||||
<div class="row">
|
/>
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="edit-country">Which <span class="kw">country</span> did you visit? <span class="req">*</span></label>
|
|
||||||
<SearchInput id="edit-country" bind:value={country} options={countryNames} required />
|
|
||||||
{#if errors.country}<span class="ferr">{errors.country}</span>{/if}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="edit-city">Which <span class="kw">cities</span> did you visit? <span class="req">*</span></label>
|
|
||||||
<SearchInput id="edit-city" bind:value={cityInput} options={cityOptions} onselect={addCity} />
|
|
||||||
{#if errors.cities}<span class="ferr">{errors.cities}</span>{/if}
|
|
||||||
{#if cities.length > 0}
|
|
||||||
<div class="tags">
|
|
||||||
{#each cities as c}
|
|
||||||
<span class="tag">{c}<button type="button" class="tag-rm" onclick={() => removeCity(c)}>×</button></span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="edit-date">When did you <span class="kw">arrive</span>? <span class="req">*</span></label>
|
|
||||||
<input id="edit-date" class="input" type="date" bind:value={date} required />
|
|
||||||
{#if errors.date}<span class="ferr">{errors.date}</span>{/if}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="edit-days">How many <span class="kw">days</span> did you stay? <span class="req">*</span></label>
|
|
||||||
<input id="edit-days" class="input" type="number" min="1" bind:value={days} required />
|
|
||||||
{#if errors.days}<span class="ferr">{errors.days}</span>{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></label>
|
|
||||||
<div class="toggle-row">
|
|
||||||
{#each ['solo','friends','family'] as t}
|
|
||||||
<label class="toggle-opt" class:active={tripType === t}>
|
|
||||||
<input type="radio" name="edit-tripType" value={t} bind:group={tripType} />
|
|
||||||
{t === 'solo' ? '🧑 Solo' : t === 'friends' ? '👥 With friends' : '👨👩👧👦 With family'}
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if errors.tripType}<span class="ferr">{errors.tripType}</span>{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></label>
|
|
||||||
<div class="transport-grid">
|
|
||||||
{#each transportOptions as opt}
|
|
||||||
<label class="toggle-opt transport-opt" class:active={transport === opt.value}>
|
|
||||||
<input type="radio" name="edit-transport" value={opt.value} bind:group={transport} />
|
|
||||||
<img src={opt.img} alt={opt.label} class="transport-img" />
|
|
||||||
{opt.label}
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if errors.transport}<span class="ferr">{errors.transport}</span>{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{:else if step === 2}
|
{:else if step === 2}
|
||||||
<h2 class="step-title">Photos</h2>
|
<h2 class="step-title">Photos</h2>
|
||||||
@@ -258,70 +143,6 @@
|
|||||||
font-family: var(--sans);
|
font-family: var(--sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 20px;
|
|
||||||
height: 52px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
.topbar-left, .topbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 110px;
|
|
||||||
}
|
|
||||||
.topbar-right { justify-content: flex-end; }
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.step-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--border);
|
|
||||||
transition: background 0.2s, transform 0.2s;
|
|
||||||
}
|
|
||||||
.step-dot.active { background: var(--accent); transform: scale(1.25); }
|
|
||||||
.step-dot.done { background: var(--accent); opacity: 0.35; }
|
|
||||||
|
|
||||||
.ghost-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--text);
|
|
||||||
background: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
.ghost-btn:hover { background: var(--bg-subtle); border-color: var(--border); color: var(--text-h); }
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--accent);
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.save-btn:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
|
|
||||||
|
|
||||||
.scroll { flex: 1; overflow-y: auto; }
|
.scroll { flex: 1; overflow-y: auto; }
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
@@ -333,13 +154,6 @@
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-headline {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-h);
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
margin: 0 0 4px;
|
|
||||||
}
|
|
||||||
.step-title {
|
.step-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -354,7 +168,6 @@
|
|||||||
margin: -10px 0 4px;
|
margin: -10px 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
||||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
.label-row {
|
.label-row {
|
||||||
@@ -370,9 +183,6 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--text-h);
|
color: var(--text-h);
|
||||||
}
|
}
|
||||||
.req { color: var(--accent); font-size: 11px; }
|
|
||||||
.kw { color: var(--accent); }
|
|
||||||
.ferr { font-size: 13px; font-weight: 500; color: #dc2626; }
|
|
||||||
|
|
||||||
.char-count { font-size: 11px; font-weight: 300; color: var(--text-sub); transition: color 0.15s; }
|
.char-count { font-size: 11px; font-weight: 300; color: var(--text-sub); transition: color 0.15s; }
|
||||||
.char-count.over { color: #dc2626; }
|
.char-count.over { color: #dc2626; }
|
||||||
@@ -395,35 +205,4 @@
|
|||||||
.input:focus { border-color: var(--accent-border); }
|
.input:focus { border-color: var(--accent-border); }
|
||||||
|
|
||||||
.textarea { resize: vertical; line-height: 1.6; }
|
.textarea { resize: vertical; line-height: 1.6; }
|
||||||
|
|
||||||
.toggle-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
||||||
.toggle-opt {
|
|
||||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
||||||
font-size: 14px; font-weight: 400; color: var(--text);
|
|
||||||
padding: 12px 14px; border-radius: 10px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
cursor: pointer; transition: border-color 0.15s, background 0.15s, color 0.15s, box-shadow 0.15s;
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.toggle-opt input { display: none; }
|
|
||||||
.toggle-opt.active { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
||||||
.toggle-opt.active img { filter: brightness(0) saturate(100%) invert(27%) sepia(98%) saturate(1169%) hue-rotate(239deg) brightness(80%) contrast(92%); }
|
|
||||||
|
|
||||||
.transport-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
|
||||||
.transport-opt { flex-direction: column; gap: 6px; padding: 16px 10px; }
|
|
||||||
.transport-img { width: 44px; height: 44px; object-fit: contain; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
||||||
.tag {
|
|
||||||
display: inline-flex; align-items: center; gap: 4px;
|
|
||||||
font-size: 12px; font-weight: 300; color: var(--accent);
|
|
||||||
background: var(--accent-bg); border: 1px solid var(--accent-border);
|
|
||||||
border-radius: 20px; padding: 3px 10px 3px 12px;
|
|
||||||
}
|
|
||||||
.tag-rm {
|
|
||||||
background: none; border: none; color: var(--accent);
|
|
||||||
font-size: 15px; line-height: 1; cursor: pointer; padding: 0; opacity: 0.6;
|
|
||||||
}
|
|
||||||
.tag-rm:hover { opacity: 1; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
<script>
|
|
||||||
/** @type {{ entries: import('../shared/types.js').JournalEntry[] }} */
|
|
||||||
let { entries } = $props();
|
|
||||||
|
|
||||||
let stats = $derived.by(() => {
|
|
||||||
if (entries.length === 0) return null;
|
|
||||||
|
|
||||||
const totalDays = entries.reduce((s, e) => s + e.days, 0);
|
|
||||||
const countries = [...new Set(entries.map(e => e.location.country))];
|
|
||||||
const cities = [...new Set(entries.flatMap(e => e.location.cities))];
|
|
||||||
|
|
||||||
const years = entries.map(e => new Date(e.date).getFullYear());
|
|
||||||
const minYear = Math.min(...years);
|
|
||||||
const maxYear = Math.max(...years);
|
|
||||||
const yearRange = minYear === maxYear ? `${minYear}` : `${minYear} – ${maxYear}`;
|
|
||||||
|
|
||||||
return { totalDays, countries, cities, yearRange, tripCount: entries.length };
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if stats}
|
|
||||||
<div class="passport">
|
|
||||||
<!-- diagonal pattern -->
|
|
||||||
|
|
||||||
<div class="passport-body">
|
|
||||||
<!-- Left -->
|
|
||||||
<div class="passport-left">
|
|
||||||
<div class="passport-header">
|
|
||||||
<svg viewBox="0 0 32 32" fill="none" class="globe">
|
|
||||||
<circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="1.3"/>
|
|
||||||
<ellipse cx="16" cy="16" rx="5.5" ry="13" stroke="currentColor" stroke-width="1.3"/>
|
|
||||||
<line x1="3" y1="16" x2="29" y2="16" stroke="currentColor" stroke-width="1.3"/>
|
|
||||||
<line x1="5" y1="9" x2="27" y2="9" stroke="currentColor" stroke-width="1.3"/>
|
|
||||||
<line x1="5" y1="23" x2="27" y2="23" stroke="currentColor" stroke-width="1.3"/>
|
|
||||||
</svg>
|
|
||||||
<span class="issuer">TRAVEL JOURNAL</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="type">PASSPORT</p>
|
|
||||||
<p class="years">{stats.yearRange}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vdivider"></div>
|
|
||||||
|
|
||||||
<!-- Right -->
|
|
||||||
<div class="passport-right">
|
|
||||||
<div class="field">
|
|
||||||
<span class="field-label">TRIPS</span>
|
|
||||||
<span class="field-value">{stats.tripCount}</span>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="field-label">COUNTRIES</span>
|
|
||||||
<span class="field-value">{stats.countries.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="field-label">DAYS</span>
|
|
||||||
<span class="field-value">{stats.totalDays}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MRZ -->
|
|
||||||
<div class="mrz">
|
|
||||||
<span>P<JNL{String(stats.tripCount).padStart(2,'0')}<<<<<<<<<<<<<<<<<<<<<<<<<<<</span>
|
|
||||||
<span>{stats.yearRange.replace(' – ','').replace(/\s/g,'')}{'<'.repeat(12)}{String(stats.totalDays).padStart(4,'0')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.passport {
|
|
||||||
background: #1e1b4b;
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #e0e7ff;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.passport::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
135deg,
|
|
||||||
transparent 0px, transparent 20px,
|
|
||||||
rgba(255,255,255,0.025) 20px, rgba(255,255,255,0.025) 21px
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Body: left + divider + right in a row */
|
|
||||||
.passport-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 20px;
|
|
||||||
gap: 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Left column */
|
|
||||||
.passport-left {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
.passport-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.globe {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
color: #a5b4fc;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.issuer {
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
color: #a5b4fc;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.type {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.22em;
|
|
||||||
color: #818cf8;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.years {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #fff;
|
|
||||||
letter-spacing: -0.8px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Divider */
|
|
||||||
.vdivider {
|
|
||||||
width: 1px;
|
|
||||||
background: rgba(255,255,255,0.12);
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right column */
|
|
||||||
.passport-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
.field-label {
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
color: #818cf8;
|
|
||||||
}
|
|
||||||
.field-value {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #fff;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MRZ strip */
|
|
||||||
.mrz {
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.1);
|
|
||||||
padding: 9px 20px;
|
|
||||||
background: rgba(0,0,0,0.18);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.mrz span {
|
|
||||||
font-family: var(--mono);
|
|
||||||
font-size: 8px;
|
|
||||||
color: #6366f1;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { journals, addJournal } from '../../stores/journalStore.js';
|
import { journals, addEntry } from '../../stores/entriesStore.svelte.js';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { flashCountry } from '../../layout/selection.svelte.js';
|
import { flashCountry } from '../../layout/selection.svelte.js';
|
||||||
import { countryNames } from '../../shared/countries.js';
|
import { countryNames } from '../../shared/countries.js';
|
||||||
import { countryCities } from '../../shared/countryCities.js';
|
import { ALL_CITIES } from '../../shared/cities.js';
|
||||||
import SearchInput from '../../shared/SearchInput.svelte';
|
import SearchInput from '../../shared/SearchInput.svelte';
|
||||||
import PhotoEditor from './PhotoEditor.svelte';
|
import PhotoEditor from './PhotoEditor.svelte';
|
||||||
|
import StepNav from './StepNavbar.svelte';
|
||||||
import airplaneImg from '../../../assets/airplane.png';
|
import airplaneImg from '../../../assets/airplane.png';
|
||||||
import trainImg from '../../../assets/train.png';
|
import trainImg from '../../../assets/train.png';
|
||||||
import busImg from '../../../assets/bus.png';
|
import busImg from '../../../assets/bus.png';
|
||||||
@@ -25,8 +26,12 @@
|
|||||||
// ── Fields ─────────────────────────────────────────────────────────
|
// ── Fields ─────────────────────────────────────────────────────────
|
||||||
let cities = $state([]);
|
let cities = $state([]);
|
||||||
let cityInput = $state('');
|
let cityInput = $state('');
|
||||||
let country = $state(initialCountry);
|
let country = $state('');
|
||||||
let date = $state(new Date().toISOString().slice(0, 10));
|
let date = $state(new Date().toISOString().slice(0, 10));
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
country = initialCountry;
|
||||||
|
});
|
||||||
let days = $state('');
|
let days = $state('');
|
||||||
let tripType = $state('');
|
let tripType = $state('');
|
||||||
let transport = $state('');
|
let transport = $state('');
|
||||||
@@ -65,7 +70,7 @@
|
|||||||
let cityOptions = $derived(
|
let cityOptions = $derived(
|
||||||
country.trim()
|
country.trim()
|
||||||
? [...new Set([
|
? [...new Set([
|
||||||
...(countryCities[country.trim()] ?? []),
|
...(ALL_CITIES[country.trim()] ?? []),
|
||||||
...journalEntries.filter(j => (j.location?.country || '').toLowerCase() === country.trim().toLowerCase()).flatMap(e => e.location?.cities ?? []),
|
...journalEntries.filter(j => (j.location?.country || '').toLowerCase() === country.trim().toLowerCase()).flatMap(e => e.location?.cities ?? []),
|
||||||
])]
|
])]
|
||||||
: []
|
: []
|
||||||
@@ -128,7 +133,7 @@
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
try {
|
try {
|
||||||
await addJournal({
|
await addEntry({
|
||||||
title: `${cities.join(', ')}, ${country}`,
|
title: `${cities.join(', ')}, ${country}`,
|
||||||
date,
|
date,
|
||||||
days: Number(days),
|
days: Number(days),
|
||||||
@@ -145,34 +150,12 @@
|
|||||||
saveError = e?.message ?? 'Failed to save. Please try again.';
|
saveError = e?.message ?? 'Failed to save. Please try again.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let next = $derived(step < 3 ? nextStep : save);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<header class="topbar">
|
<StepNav {step} onback={prevStep} onnext={next} {saving} saveLabel="Save trip" {saveError} />
|
||||||
<div class="topbar-left">
|
|
||||||
<button class="ghost-btn" onclick={prevStep}>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
|
||||||
{step === 1 ? 'Back' : 'Previous'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="steps">
|
|
||||||
{#each [1,2,3] as s}
|
|
||||||
<div class="step-dot" class:active={step === s} class:done={step > s}></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="topbar-right">
|
|
||||||
{#if step < 3}
|
|
||||||
<button class="save-btn" onclick={nextStep}>Next</button>
|
|
||||||
{:else}
|
|
||||||
<button class="save-btn" onclick={save} disabled={saving}>
|
|
||||||
{saving ? 'Saving…' : 'Save trip'}
|
|
||||||
</button>
|
|
||||||
{#if saveError}<span class="save-err">{saveError}</span>{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
@@ -283,78 +266,6 @@
|
|||||||
font-family: var(--sans);
|
font-family: var(--sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* topbar */
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 20px;
|
|
||||||
height: 52px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
.topbar-left, .topbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 110px;
|
|
||||||
}
|
|
||||||
.topbar-right { justify-content: flex-end; }
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.step-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--border);
|
|
||||||
transition: background 0.2s, transform 0.2s;
|
|
||||||
}
|
|
||||||
.step-dot.active {
|
|
||||||
background: var(--accent);
|
|
||||||
transform: scale(1.25);
|
|
||||||
}
|
|
||||||
.step-dot.done {
|
|
||||||
background: var(--accent);
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--text);
|
|
||||||
background: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
.ghost-btn:hover { background: var(--bg-subtle); border-color: var(--border); color: var(--text-h); }
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--accent);
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.save-btn:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
|
|
||||||
.save-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* scroll + form */
|
/* scroll + form */
|
||||||
.scroll { flex: 1; overflow-y: auto; }
|
.scroll { flex: 1; overflow-y: auto; }
|
||||||
|
|
||||||
|
|||||||
100
src/lib/timeline/detail/StepNavbar.svelte
Normal file
100
src/lib/timeline/detail/StepNavbar.svelte
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script>
|
||||||
|
let { step, totalSteps = 3, onback, onnext, saving = false, saveLabel = 'Save changes', saveError = '' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="topbar-left">
|
||||||
|
<button class="ghost-btn" onclick={onback}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
||||||
|
{step === 1 ? 'Back' : 'Previous'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="steps">
|
||||||
|
{#each Array(totalSteps) as _, i}
|
||||||
|
<div class="step-dot" class:active={step === i + 1} class:done={step > i + 1}></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="topbar-right">
|
||||||
|
<button class="save-btn" onclick={onnext} disabled={saving}>
|
||||||
|
{#if saving}
|
||||||
|
Saving…
|
||||||
|
{:else if step < totalSteps}
|
||||||
|
Next
|
||||||
|
{:else}
|
||||||
|
{saveLabel}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if saveError}<span class="save-err">{saveError}</span>{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 52px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.topbar-left, .topbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 110px;
|
||||||
|
}
|
||||||
|
.topbar-right { justify-content: flex-end; }
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.step-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border);
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.step-dot.active { background: var(--accent); transform: scale(1.25); }
|
||||||
|
.step-dot.done { background: var(--accent); opacity: 0.35; }
|
||||||
|
|
||||||
|
.ghost-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.ghost-btn:hover { background: var(--bg-subtle); border-color: var(--border); color: var(--text-h); }
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--accent);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.save-btn:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
|
||||||
|
.save-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.save-err { font-size: 12px; color: #dc2626; white-space: nowrap; }
|
||||||
|
</style>
|
||||||
188
src/lib/timeline/detail/TripBasicInfo.svelte
Normal file
188
src/lib/timeline/detail/TripBasicInfo.svelte
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<script>
|
||||||
|
import { countryNames } from '../../shared/countries.js';
|
||||||
|
import { getCitiesForCountry, ALL_CITIES } from '../../shared/cities.js';
|
||||||
|
import { getEntries } from '../../stores/entriesStore.svelte.js';
|
||||||
|
import SearchInput from '../../shared/SearchInput.svelte';
|
||||||
|
import airplaneImg from '../../../assets/airplane.png';
|
||||||
|
import trainImg from '../../../assets/train.png';
|
||||||
|
import busImg from '../../../assets/bus.png';
|
||||||
|
import carImg from '../../../assets/car.png';
|
||||||
|
import shipImg from '../../../assets/ship.png';
|
||||||
|
import walkImg from '../../../assets/walk.png';
|
||||||
|
|
||||||
|
let {
|
||||||
|
country = $bindable(''),
|
||||||
|
cities = $bindable([]),
|
||||||
|
date = $bindable(''),
|
||||||
|
days = $bindable(''),
|
||||||
|
tripType = $bindable(''),
|
||||||
|
transport = $bindable(''),
|
||||||
|
errors = $bindable({ country: '', cities: '', date: '', days: '', tripType: '', transport: '' }),
|
||||||
|
isNew = false,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let cityInput = $state('');
|
||||||
|
|
||||||
|
const transportOptions = [
|
||||||
|
{ value: 'flight', label: 'Flight', img: airplaneImg },
|
||||||
|
{ value: 'train', label: 'Train', img: trainImg },
|
||||||
|
{ value: 'bus', label: 'Bus', img: busImg },
|
||||||
|
{ value: 'car', label: 'Car', img: carImg },
|
||||||
|
{ value: 'ship', label: 'Ship', img: shipImg },
|
||||||
|
{ value: 'walk', label: 'Walk', img: walkImg },
|
||||||
|
];
|
||||||
|
|
||||||
|
let allEntries = $derived(getEntries());
|
||||||
|
let cityOptions = $derived(
|
||||||
|
country.trim()
|
||||||
|
? [...new Set([...getCitiesForCountry(country), ...allEntries.filter(j => (j.location.country || '').toLowerCase() === country.trim().toLowerCase()).flatMap(e => e.location.cities)])].sort()
|
||||||
|
: [...new Set([...Object.values(ALL_CITIES).flat(), ...allEntries.flatMap(e => e.location.cities)])].sort()
|
||||||
|
);
|
||||||
|
|
||||||
|
function addCity(val) {
|
||||||
|
const trimmed = (val ?? cityInput).trim();
|
||||||
|
if (trimmed && !cities.includes(trimmed)) {
|
||||||
|
cities = [...cities, trimmed];
|
||||||
|
}
|
||||||
|
cityInput = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCity(c) {
|
||||||
|
cities = cities.filter(x => x !== c);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="page-headline">
|
||||||
|
{isNew ? 'Journal your trip!' : 'Edit your trip'}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="tbi-country">Which <span class="kw">country</span> did you visit? <span class="req">*</span></label>
|
||||||
|
<SearchInput id="tbi-country" bind:value={country} options={countryNames} required />
|
||||||
|
{#if errors.country}<span class="ferr">{errors.country}</span>{/if}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="tbi-city">Which <span class="kw">cities</span> did you visit? <span class="req">*</span></label>
|
||||||
|
<SearchInput id="tbi-city" bind:value={cityInput} options={cityOptions} onselect={addCity} />
|
||||||
|
{#if errors.cities}<span class="ferr">{errors.cities}</span>{/if}
|
||||||
|
{#if cities.length > 0}
|
||||||
|
<div class="tags">
|
||||||
|
{#each cities as c}
|
||||||
|
<span class="tag">{c}<button type="button" class="tag-rm" onclick={() => removeCity(c)}>×</button></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="tbi-date">When did you <span class="kw">arrive</span>? <span class="req">*</span></label>
|
||||||
|
<input id="tbi-date" class="input" type="date" bind:value={date} required />
|
||||||
|
{#if errors.date}<span class="ferr">{errors.date}</span>{/if}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="tbi-days">How many <span class="kw">days</span> did you stay? <span class="req">*</span></label>
|
||||||
|
<input id="tbi-days" class="input" type="number" min="1" bind:value={days} required />
|
||||||
|
{#if errors.days}<span class="ferr">{errors.days}</span>{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></label>
|
||||||
|
<div class="toggle-row">
|
||||||
|
{#each ['solo','friends','family'] as t}
|
||||||
|
<label class="toggle-opt" class:active={tripType === t}>
|
||||||
|
<input type="radio" name="tbi-tripType" value={t} bind:group={tripType} />
|
||||||
|
{t === 'solo' ? '🧑 Solo' : t === 'friends' ? '👥 With friends' : '👨👩👧👦 With family'}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if errors.tripType}<span class="ferr">{errors.tripType}</span>{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></label>
|
||||||
|
<div class="transport-grid">
|
||||||
|
{#each transportOptions as opt}
|
||||||
|
<label class="toggle-opt transport-opt" class:active={transport === opt.value}>
|
||||||
|
<input type="radio" name="tbi-transport" value={opt.value} bind:group={transport} />
|
||||||
|
<img src={opt.img} alt={opt.label} class="transport-img" />
|
||||||
|
{opt.label}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if errors.transport}<span class="ferr">{errors.transport}</span>{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-headline {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-h);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||||
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
.req { color: var(--accent); font-size: 11px; }
|
||||||
|
.kw { color: var(--accent); }
|
||||||
|
.ferr { font-size: 13px; font-weight: 500; color: #dc2626; }
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--text-h);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.input:focus { border-color: var(--accent-border); }
|
||||||
|
|
||||||
|
.toggle-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.toggle-opt {
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
|
font-size: 14px; font-weight: 400; color: var(--text);
|
||||||
|
padding: 12px 14px; border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
cursor: pointer; transition: border-color 0.15s, background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.toggle-opt input { display: none; }
|
||||||
|
.toggle-opt.active { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||||||
|
.toggle-opt.active img { filter: brightness(0) saturate(100%) invert(27%) sepia(98%) saturate(1169%) hue-rotate(239deg) brightness(80%) contrast(92%); }
|
||||||
|
|
||||||
|
.transport-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||||
|
.transport-opt { flex-direction: column; gap: 6px; padding: 16px 10px; }
|
||||||
|
.transport-img { width: 44px; height: 44px; object-fit: contain; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
||||||
|
.tag {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
font-size: 12px; font-weight: 300; color: var(--accent);
|
||||||
|
background: var(--accent-bg); border: 1px solid var(--accent-border);
|
||||||
|
border-radius: 20px; padding: 3px 10px 3px 12px;
|
||||||
|
}
|
||||||
|
.tag-rm {
|
||||||
|
background: none; border: none; color: var(--accent);
|
||||||
|
font-size: 15px; line-height: 1; cursor: pointer; padding: 0; opacity: 0.6;
|
||||||
|
}
|
||||||
|
.tag-rm:hover { opacity: 1; }
|
||||||
|
</style>
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script>
|
|
||||||
const sortOptions = [
|
|
||||||
{ value: 'date-desc', label: 'Newest first' },
|
|
||||||
{ value: 'date-asc', label: 'Oldest first' },
|
|
||||||
{ value: 'country-asc', label: 'Country A → Z' },
|
|
||||||
{ value: 'country-desc', label: 'Country Z → A' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let { sortKey, onSort } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="toolbar"></div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.toolbar {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
font-weight: 300;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 6px 28px 6px 12px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--bg-subtle);
|
|
||||||
color: var(--text);
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2352525b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 10px center;
|
|
||||||
transition: border-color 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
select:hover { border-color: var(--border-bright); color: var(--text-h); }
|
|
||||||
select:focus { outline: 1px solid var(--accent-border); outline-offset: 2px; }
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getEntries } from '../../stores/entriesStore.svelte.js';
|
import { getEntries } from '../../stores/entriesStore.svelte.js';
|
||||||
import TimelineToolbar from './TimelineToolbar.svelte';
|
|
||||||
import TimelineCard from './TimelineCard.svelte';
|
import TimelineCard from './TimelineCard.svelte';
|
||||||
import JournalDetail from '../detail/JournalDetail.svelte';
|
import JournalDetail from '../detail/JournalDetail.svelte';
|
||||||
import EditForm from '../detail/EditForm.svelte';
|
import EditForm from '../detail/EditForm.svelte';
|
||||||
@@ -78,8 +77,6 @@
|
|||||||
|
|
||||||
<div class="two-col">
|
<div class="two-col">
|
||||||
<div class="left-col">
|
<div class="left-col">
|
||||||
<TimelineToolbar {sortKey} onSort={(k) => (sortKey = k)} />
|
|
||||||
|
|
||||||
{#if sortedEntries.length === 0}
|
{#if sortedEntries.length === 0}
|
||||||
<p class="empty">No journal entries yet.</p>
|
<p class="empty">No journal entries yet.</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { feature } from 'topojson-client';
|
import { feature } from 'topojson-client';
|
||||||
import worldData from 'world-atlas/countries-50m.json';
|
import worldData from 'world-atlas/countries-50m.json';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { journals } from '../stores/journalStore.js';
|
import { journals } from '../stores/entriesStore.svelte.js';
|
||||||
import airplaneImg from '../../assets/airplane-animation.png';
|
import airplaneImg from '../../assets/airplane-animation.png';
|
||||||
|
|
||||||
let { onclose, onprogress, mode = 'map', onmodechange } = $props();
|
let { onclose, onprogress, mode = 'map', onmodechange } = $props();
|
||||||
|
|||||||
Reference in New Issue
Block a user