diff --git a/Makefile b/Makefile index 273a3b8..82c9ed5 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ dev: pnpm run dev migrate-local: - cd apps/api && pnpm wrangler d1 execute tabitabi --local --file=../../migrations/0001_simple_schema.sql + cd apps/api && pnpm wrangler d1 migrations apply tabitabi --local migrate-remote: - cd apps/api && pnpm wrangler d1 execute tabitabi --remote --file=../../migrations/0001_simple_schema.sql + cd apps/api && pnpm wrangler d1 migrations apply tabitabi --remote diff --git a/migrations/0001_simple_schema.sql b/apps/api/migrations/0001_simple_schema.sql similarity index 100% rename from migrations/0001_simple_schema.sql rename to apps/api/migrations/0001_simple_schema.sql diff --git a/migrations/0002_add_memo_password.sql b/apps/api/migrations/0002_add_memo_password.sql similarity index 100% rename from migrations/0002_add_memo_password.sql rename to apps/api/migrations/0002_add_memo_password.sql diff --git a/migrations/0003_add_secret_mode.sql b/apps/api/migrations/0003_add_secret_mode.sql similarity index 86% rename from migrations/0003_add_secret_mode.sql rename to apps/api/migrations/0003_add_secret_mode.sql index 191aba8..c8d3d5d 100644 --- a/migrations/0003_add_secret_mode.sql +++ b/apps/api/migrations/0003_add_secret_mode.sql @@ -1,7 +1,7 @@ -- Migration: Create itinerary_secrets table -- Date: 2025-11-24 -CREATE TABLE itinerary_secrets ( +CREATE TABLE IF NOT EXISTS itinerary_secrets ( itinerary_id TEXT PRIMARY KEY, enabled BOOLEAN DEFAULT FALSE, offset_minutes INTEGER DEFAULT 60, diff --git a/apps/api/migrations/0004_add_walica_id.sql b/apps/api/migrations/0004_add_walica_id.sql new file mode 100644 index 0000000..45fb292 --- /dev/null +++ b/apps/api/migrations/0004_add_walica_id.sql @@ -0,0 +1,2 @@ +-- Add walica_id column to itineraries table +ALTER TABLE itineraries ADD COLUMN walica_id TEXT; diff --git a/apps/api/migrations/0005_refactor_walica.sql b/apps/api/migrations/0005_refactor_walica.sql new file mode 100644 index 0000000..9b6b659 --- /dev/null +++ b/apps/api/migrations/0005_refactor_walica.sql @@ -0,0 +1,19 @@ +-- Migration: Refactor Walica to separate table +-- Date: 2025-11-24 + +CREATE TABLE itinerary_walica_settings ( + itinerary_id TEXT PRIMARY KEY, + walica_id TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (itinerary_id) REFERENCES itineraries(id) ON DELETE CASCADE +); + +-- Migrate existing data +INSERT INTO itinerary_walica_settings (itinerary_id, walica_id, created_at, updated_at) +SELECT id, walica_id, created_at, updated_at +FROM itineraries +WHERE walica_id IS NOT NULL; + +-- Drop column from itineraries +ALTER TABLE itineraries DROP COLUMN walica_id; diff --git a/apps/api/src/services/itinerary.service.ts b/apps/api/src/services/itinerary.service.ts index cea40a0..9d847c8 100644 --- a/apps/api/src/services/itinerary.service.ts +++ b/apps/api/src/services/itinerary.service.ts @@ -8,9 +8,12 @@ export class ItineraryService { async list(): Promise { const result = await this.db .prepare(` - SELECT i.*, s.enabled as secret_enabled, s.offset_minutes as secret_offset + SELECT i.*, + s.enabled as secret_enabled, s.offset_minutes as secret_offset, + w.walica_id as walica_id FROM itineraries i LEFT JOIN itinerary_secrets s ON i.id = s.itinerary_id + LEFT JOIN itinerary_walica_settings w ON i.id = w.itinerary_id ORDER BY i.created_at DESC `) .all(); @@ -21,9 +24,12 @@ export class ItineraryService { async get(id: string): Promise { const result = await this.db .prepare(` - SELECT i.*, s.enabled as secret_enabled, s.offset_minutes as secret_offset + SELECT i.*, + s.enabled as secret_enabled, s.offset_minutes as secret_offset, + w.walica_id as walica_id FROM itineraries i LEFT JOIN itinerary_secrets s ON i.id = s.itinerary_id + LEFT JOIN itinerary_walica_settings w ON i.id = w.itinerary_id WHERE i.id = ? `) .bind(id) @@ -41,6 +47,7 @@ export class ItineraryService { title: input.title, theme_id: input.theme_id || 'minimal', memo: input.memo ?? null, + walica_id: input.walica_id ?? null, password: input.password ?? null, secret_settings: input.secret_settings ? { enabled: input.secret_settings.enabled, @@ -70,6 +77,14 @@ export class ItineraryService { .run(); } + // Insert into walica table if exists + if (itinerary.walica_id) { + await this.db + .prepare('INSERT INTO itinerary_walica_settings (itinerary_id, walica_id, created_at, updated_at) VALUES (?, ?, ?, ?)') + .bind(itinerary.id, itinerary.walica_id, now, now) + .run(); + } + return itinerary; } @@ -116,9 +131,6 @@ export class ItineraryService { .run(); } else { // Upsert settings - // Check if exists first (D1 doesn't support INSERT OR REPLACE nicely with timestamps preservation if we want that, but here we just overwrite) - // Actually, standard SQL UPSERT or just DELETE+INSERT or UPDATE/INSERT check. - // Let's try INSERT OR REPLACE await this.db .prepare(` INSERT INTO itinerary_secrets (itinerary_id, enabled, offset_minutes, created_at, updated_at) @@ -139,12 +151,34 @@ export class ItineraryService { } } + // Handle walica settings update + if (input.walica_id !== undefined) { + if (input.walica_id === null) { + // Remove settings + await this.db + .prepare('DELETE FROM itinerary_walica_settings WHERE itinerary_id = ?') + .bind(id) + .run(); + } else { + // Upsert settings + await this.db + .prepare(` + INSERT INTO itinerary_walica_settings (itinerary_id, walica_id, created_at, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(itinerary_id) DO UPDATE SET + walica_id = excluded.walica_id, + updated_at = excluded.updated_at + `) + .bind(id, input.walica_id, now, now) + .run(); + } + } + return await this.get(id); } async delete(id: string): Promise { - // Foreign key cascade should handle the secrets table, but let's be safe or rely on DB - // Since we defined ON DELETE CASCADE in migration, deleting from itineraries is enough. + // Foreign key cascade should handle the secrets and walica tables const result = await this.db .prepare('DELETE FROM itineraries WHERE id = ?') .bind(id) @@ -159,6 +193,7 @@ export class ItineraryService { title: row.title, theme_id: row.theme_id, memo: row.memo, + walica_id: row.walica_id, password: row.password, created_at: row.created_at, updated_at: row.updated_at, diff --git a/apps/api/wrangler.toml.example b/apps/api/wrangler.toml.example index 8a78991..9ee0b24 100644 --- a/apps/api/wrangler.toml.example +++ b/apps/api/wrangler.toml.example @@ -9,7 +9,7 @@ enabled = true binding = "DB" database_name = "tabitabi" database_id = "PLACEHOLDER_DATABASE_ID" -migrations_dir = "../../migrations" +migrations_dir = "./migrations" [vars] ALLOWED_ORIGINS = "*" @@ -27,4 +27,4 @@ enabled = true binding = "DB" database_name = "tabitabi" database_id = "PLACEHOLDER_DATABASE_ID" -migrations_dir = "../../migrations" +migrations_dir = "./migrations" diff --git a/apps/web/src/lib/themes/standard-autumn/ItineraryView.svelte b/apps/web/src/lib/themes/standard-autumn/ItineraryView.svelte index 9247875..eadac18 100644 --- a/apps/web/src/lib/themes/standard-autumn/ItineraryView.svelte +++ b/apps/web/src/lib/themes/standard-autumn/ItineraryView.svelte @@ -16,6 +16,7 @@ title?: string; theme_id?: string; memo?: string; + walica_id?: string | null; secret_settings?: { enabled: boolean; offset_minutes: number; @@ -73,6 +74,11 @@ itinerary.secret_settings?.offset_minutes ?? 60, ); + let walicaUrl = $state( + itinerary.walica_id ? `https://walica.jp/group/${itinerary.walica_id}` : "", + ); + let showWalica = $state(false); + let newStep = $state({ title: "", date: "", @@ -300,6 +306,20 @@ } } + async function handleWalicaUpdate() { + // Basic validation for walica.jp domain + if (walicaUrl && !walicaUrl.startsWith("https://walica.jp/group/")) { + alert("WalicaのURLは https://walica.jp/group/ で始まる必要があります"); + return; + } + + const walicaId = walicaUrl ? walicaUrl.split("/group/")[1] : null; + + if (onUpdateItinerary) { + await onUpdateItinerary({ walica_id: walicaId }); + } + } + // Configure marked options marked.setOptions({ breaks: true, @@ -519,6 +539,26 @@ Calendar --> + {#if itinerary.walica_id} + + {/if} +
+ +
+ +
+ +
{/if} {#if showThemeSelect} @@ -771,6 +828,39 @@ {/if} + {#if showWalica && itinerary.walica_id} +
+
+ + Walica +
+ +
+ {/if} + {#if showShareDialog} diff --git a/apps/web/src/lib/themes/standard-autumn/theme.css b/apps/web/src/lib/themes/standard-autumn/theme.css index 6b8ac23..b9128b1 100644 --- a/apps/web/src/lib/themes/standard-autumn/theme.css +++ b/apps/web/src/lib/themes/standard-autumn/theme.css @@ -1303,3 +1303,65 @@ body { font-size: 1.5rem; } } + +/* Walica Overlay */ +.standard-autumn-walica-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #fff; + z-index: 2000; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +.standard-autumn-walica-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: var(--standard-autumn-bg); + border-bottom: 1px solid var(--standard-autumn-border); + box-shadow: var(--standard-autumn-shadow-sm); +} + +.standard-autumn-walica-close-btn { + display: flex; + align-items: center; + gap: 0.5rem; + background: none; + border: none; + font-size: 1rem; + font-weight: 600; + color: var(--standard-autumn-primary); + cursor: pointer; + padding: 0.5rem; + border-radius: 8px; + transition: background 0.2s; +} + +.standard-autumn-walica-close-btn:hover { + background: rgba(169, 53, 41, 0.1); +} + +.standard-autumn-walica-title { + font-family: "Hiragino Mincho ProN", "Yu Mincho", serif; + font-weight: 600; + font-size: 1.1rem; + color: var(--standard-autumn-text); +} + +.standard-autumn-walica-frame { + flex: 1; + width: 100%; + border: none; + background: #fff; +} diff --git a/apps/web/src/routes/[id]/+page.svelte b/apps/web/src/routes/[id]/+page.svelte index 2cf4457..d9e1386 100644 --- a/apps/web/src/routes/[id]/+page.svelte +++ b/apps/web/src/routes/[id]/+page.svelte @@ -20,28 +20,32 @@ steps = data.steps; }); - onMount(async () => { - auth.updateAccessTime(data.itinerary.id, data.itinerary.title); - document.body.style.backgroundColor = backgroundColor; - document.documentElement.style.backgroundColor = backgroundColor; - - // Check if we have edit permission and need to re-fetch steps to reveal secrets - const token = - auth.extractTokenFromUrl() || auth.getToken(data.itinerary.id); - if (token) { - // If we have a token, we might be in edit mode. - // If the initial load was SSR, steps might be masked. - // We should re-fetch to get the unmasked data. - // We can check if any step is hidden or just force re-fetch if secret mode is enabled. - if (data.itinerary.secret_settings?.enabled) { - try { - const unmaskedSteps = await stepApi.list(data.itinerary.id); - steps = unmaskedSteps; - } catch (e) { - console.error("Failed to re-fetch steps:", e); + onMount(() => { + const init = async () => { + auth.updateAccessTime(data.itinerary.id, data.itinerary.title); + document.body.style.backgroundColor = backgroundColor; + document.documentElement.style.backgroundColor = backgroundColor; + + // Check if we have edit permission and need to re-fetch steps to reveal secrets + const token = + auth.extractTokenFromUrl() || auth.getToken(data.itinerary.id); + if (token) { + // If we have a token, we might be in edit mode. + // If the initial load was SSR, steps might be masked. + // We should re-fetch to get the unmasked data. + // We can check if any step is hidden or just force re-fetch if secret mode is enabled. + if (data.itinerary.secret_settings?.enabled) { + try { + const unmaskedSteps = await stepApi.list(data.itinerary.id); + steps = unmaskedSteps; + } catch (e) { + console.error("Failed to re-fetch steps:", e); + } } } - } + }; + + init(); return () => { document.body.style.backgroundColor = ""; @@ -58,6 +62,11 @@ title?: string; theme_id?: string; memo?: string; + walica_id?: string | null; + secret_settings?: { + enabled: boolean; + offset_minutes: number; + } | null; }) { try { await itineraryApi.update(data.itinerary.id, updateData); diff --git a/docs/database.md b/docs/database.md index c179184..8d1aa5b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -5,6 +5,8 @@ ```mermaid erDiagram itineraries ||--o{ steps : contains + itineraries ||--o| itinerary_secrets : has + itineraries ||--o| itinerary_walica_settings : has itineraries { TEXT id PK @@ -16,6 +18,21 @@ erDiagram TEXT updated_at } + itinerary_secrets { + TEXT itinerary_id PK, FK + BOOLEAN enabled + INTEGER offset_minutes + TEXT created_at + TEXT updated_at + } + + itinerary_walica_settings { + TEXT itinerary_id PK, FK + TEXT walica_id + TEXT created_at + TEXT updated_at + } + steps { TEXT id PK TEXT itinerary_id FK diff --git a/packages/types/src/itinerary.ts b/packages/types/src/itinerary.ts index b27e302..3e77731 100644 --- a/packages/types/src/itinerary.ts +++ b/packages/types/src/itinerary.ts @@ -8,6 +8,7 @@ export interface Itinerary { title: string; theme_id: string; memo?: string | null; + walica_id?: string | null; password?: string | null; secret_settings?: ItinerarySecretSettings | null; created_at: string; @@ -18,6 +19,7 @@ export interface CreateItineraryInput { title: string; theme_id?: string; // オプション、デフォルト: 'minimal' memo?: string; + walica_id?: string; password?: string; secret_settings?: { enabled: boolean; @@ -29,9 +31,10 @@ export interface UpdateItineraryInput { title?: string; theme_id?: string; memo?: string; + walica_id?: string | null; password?: string; secret_settings?: { enabled: boolean; offset_minutes: number; - }; + } | null; }