Skip to content

Commit b3665dd

Browse files
committed
feat: add recent itineraries functionality and navigation buttons to themes
1 parent 966ceef commit b3665dd

8 files changed

Lines changed: 197 additions & 5 deletions

File tree

apps/web/src/lib/themes/minimal/ItineraryView.svelte

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { goto } from "$app/navigation";
23
import type { Itinerary, Step } from "@tabitabi/types";
34
import { getAvailableThemes } from "$lib/themes";
45
import StepList from "./StepList.svelte";
@@ -58,7 +59,7 @@
5859
let newStepMinute = $state("00");
5960
6061
$effect(() => {
61-
newStep.time = `${newStepHour}:${newStepMinute}`;
62+
newStep.time = `\${newStepHour}:\${newStepMinute}`;
6263
});
6364
6465
async function handleTitleUpdate() {
@@ -98,7 +99,6 @@
9899
notes: newStep.notes.trim() || undefined,
99100
});
100101
101-
// フォームをリセット
102102
newStep = { title: "", date: "", time: "", location: "", notes: "" };
103103
newStepHour = "09";
104104
newStepMinute = "00";
@@ -115,6 +115,17 @@
115115
</script>
116116

117117
<div class="minimal-theme">
118+
<nav class="minimal-nav">
119+
<button
120+
type="button"
121+
onclick={() => goto("/")}
122+
class="minimal-home-btn"
123+
title="ホームに戻る"
124+
>
125+
← ホーム
126+
</button>
127+
</nav>
128+
118129
<header class="minimal-header">
119130
{#if isEditingTitle}
120131
<input

apps/web/src/lib/themes/minimal/theme.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* ミニマルテーマ - 超軽量スタイル */
1+
/* ミニマルテーマ */
22

33
.minimal-theme {
44
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
@@ -253,3 +253,26 @@
253253
padding: 0.375rem 0.75rem;
254254
}
255255
}
256+
257+
/* ナビゲーション */
258+
.minimal-nav {
259+
margin-bottom: 1rem;
260+
}
261+
262+
.minimal-home-btn {
263+
padding: 0.5rem 1rem;
264+
background: none;
265+
border: 1px solid #ddd;
266+
border-radius: 8px;
267+
cursor: pointer;
268+
transition: all 0.2s;
269+
font-family: inherit;
270+
font-size: 0.875rem;
271+
color: #666;
272+
}
273+
274+
.minimal-home-btn:hover {
275+
background: #f5f5f5;
276+
border-color: #999;
277+
color: #333;
278+
}

apps/web/src/lib/themes/standard/ItineraryView.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import { goto } from "$app/navigation";
3+
24
import type { Itinerary, Step } from "@tabitabi/types";
35
import { getAvailableThemes } from "$lib/themes";
46
import StepList from "./StepList.svelte";
@@ -115,8 +117,20 @@
115117

116118
<div class="standard-theme">
117119
<div class="standard-container">
120+
<nav class="standard-nav">
121+
<button
122+
type="button"
123+
onclick={() => goto("/")}
124+
class="standard-home-btn"
125+
title="ホームに戻る"
126+
>
127+
← ホーム
128+
</button>
129+
</nav>
130+
118131
<header class="standard-header">
119132
<div class="standard-header-content">
133+
120134
{#if isEditingTitle}
121135
<input
122136
type="text"

apps/web/src/lib/themes/standard/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const standardTheme: Theme = {
66
id: 'standard',
77
name: 'スタンダード',
88
version: '1.0.0',
9-
description: 'ふわふわとした色合いのオシャレなテーマ',
9+
description: 'デフォルトテーマ',
1010
author: 'Tabitabi',
1111
features: {
1212
steps: {

apps/web/src/lib/themes/standard/theme.css

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* スタンダードテーマ - ふわふわとした色合いのオシャレなデザイン */
1+
/* スタンダードテーマ */
22

33
.standard-theme {
44
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif;
@@ -432,3 +432,28 @@
432432
width: 100%;
433433
}
434434
}
435+
436+
/* ナビゲーション */
437+
.standard-nav {
438+
margin-bottom: 1.5rem;
439+
}
440+
441+
.standard-home-btn {
442+
padding: 0.625rem 1.25rem;
443+
background: linear-gradient(135deg, #fff 0%, #faf5ff 100%);
444+
border: 2px solid rgba(167, 139, 250, 0.2);
445+
border-radius: 10px;
446+
cursor: pointer;
447+
transition: all 0.2s;
448+
font-family: inherit;
449+
font-size: 0.875rem;
450+
font-weight: 500;
451+
color: #7c3aed;
452+
}
453+
454+
.standard-home-btn:hover {
455+
background: linear-gradient(135deg, #faf5ff 0%, #f5edff 100%);
456+
border-color: #a78bfa;
457+
box-shadow: 0 2px 8px rgba(167, 139, 250, 0.12);
458+
transform: translateY(-1px);
459+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// LocalStorage utility for recent itineraries
2+
export interface RecentItinerary {
3+
id: string;
4+
title: string;
5+
visitedAt: string;
6+
}
7+
8+
const STORAGE_KEY = 'tabitabi_recent_itineraries';
9+
const MAX_RECENT = 5;
10+
11+
export function saveRecentItinerary(id: string, title: string): void {
12+
if (typeof window === 'undefined') return;
13+
14+
try {
15+
const recent = getRecentItineraries();
16+
17+
// Remove existing entry with same id
18+
const filtered = recent.filter(item => item.id !== id);
19+
20+
// Add new entry at the beginning
21+
const updated: RecentItinerary[] = [
22+
{ id, title, visitedAt: new Date().toISOString() },
23+
...filtered
24+
].slice(0, MAX_RECENT);
25+
26+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
27+
} catch (error) {
28+
console.error('Failed to save recent itinerary:', error);
29+
}
30+
}
31+
32+
export function getRecentItineraries(): RecentItinerary[] {
33+
if (typeof window === 'undefined') return [];
34+
35+
try {
36+
const stored = localStorage.getItem(STORAGE_KEY);
37+
if (!stored) return [];
38+
39+
const items = JSON.parse(stored) as RecentItinerary[];
40+
return Array.isArray(items) ? items : [];
41+
} catch (error) {
42+
console.error('Failed to load recent itineraries:', error);
43+
return [];
44+
}
45+
}
46+
47+
export function clearRecentItineraries(): void {
48+
if (typeof window === 'undefined') return;
49+
50+
try {
51+
localStorage.removeItem(STORAGE_KEY);
52+
} catch (error) {
53+
console.error('Failed to clear recent itineraries:', error);
54+
}
55+
}

apps/web/src/routes/+page.svelte

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
<script lang="ts">
22
import { goto } from "$app/navigation";
3+
import { onMount } from "svelte";
34
import { itineraryApi } from "$lib/api/itinerary";
45
import { getAvailableThemes } from "$lib/themes";
6+
import {
7+
getRecentItineraries,
8+
type RecentItinerary,
9+
} from "$lib/utils/recentItineraries";
510
611
let title = $state("");
712
let theme_id = $state("standard");
813
let creating = $state(false);
14+
let recentItineraries = $state<RecentItinerary[]>([]);
15+
let showRecent = $state(false);
916
1017
const themes = getAvailableThemes();
1118
19+
// Load recent itineraries with delay for performance
20+
onMount(() => {
21+
setTimeout(() => {
22+
recentItineraries = getRecentItineraries();
23+
showRecent = true;
24+
}, 300);
25+
});
26+
1227
async function createItinerary() {
1328
if (!title.trim()) return;
1429
@@ -77,5 +92,47 @@
7792
URLが発行されます。仲間と共有しよう!
7893
</p>
7994
</div>
95+
96+
<!-- Recent Itineraries -->
97+
{#if showRecent && recentItineraries.length > 0}
98+
<div class="mt-8 bg-white rounded-2xl shadow-xl p-6 animate-fade-in">
99+
<h2 class="text-xl font-semibold text-gray-800 mb-4">📚 最近の項目</h2>
100+
<div class="space-y-2">
101+
{#each recentItineraries as item}
102+
<button
103+
onclick={() => goto(`/${item.id}`)}
104+
class="w-full text-left px-4 py-3 rounded-lg bg-gray-50 hover:bg-indigo-50 hover:border-indigo-200 border-2 border-transparent transition-all duration-200"
105+
>
106+
<div class="font-medium text-gray-800">{item.title}</div>
107+
<div class="text-xs text-gray-500 mt-1">
108+
{new Date(item.visitedAt).toLocaleDateString("ja-JP", {
109+
month: "short",
110+
day: "numeric",
111+
hour: "2-digit",
112+
minute: "2-digit",
113+
})}
114+
</div>
115+
</button>
116+
{/each}
117+
</div>
118+
</div>
119+
{/if}
80120
</div>
81121
</div>
122+
123+
<style>
124+
@keyframes fade-in {
125+
from {
126+
opacity: 0;
127+
transform: translateY(10px);
128+
}
129+
to {
130+
opacity: 1;
131+
transform: translateY(0);
132+
}
133+
}
134+
135+
.animate-fade-in {
136+
animation: fade-in 0.4s ease-out;
137+
}
138+
</style>

apps/web/src/routes/[id]/+page.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
import { invalidateAll } from "$app/navigation";
33
import { itineraryApi } from "$lib/api/itinerary";
44
import { stepApi } from "$lib/api/step";
5+
import { saveRecentItinerary } from "$lib/utils/recentItineraries";
6+
import { onMount } from "svelte";
57
68
let { data } = $props();
79
810
let ItineraryView = $derived(data.theme.components.ItineraryView);
911
12+
// Save to recent itineraries on mount
13+
onMount(() => {
14+
saveRecentItinerary(data.itinerary.id, data.itinerary.title);
15+
});
16+
1017
async function handleUpdateItinerary(updateData: {
1118
title?: string;
1219
theme_id?: string;

0 commit comments

Comments
 (0)