diff --git a/README.md b/README.md index de63283..b7a9019 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,10 @@ docker compose up -d | 🛬 到站清单 | 落地第一周按天勾选 | | ☕ 咖啡馆 | 办公友好咖啡馆推荐 | | 👋 新手引导 | 首次访问四步导览 | +| 🔌 电源插座 | 插头类型 / 电压 / 适配建议 | +| 🆘 紧急联系 | 急救、医院、领事馆速查 | +| 📝 城市笔记 | 按城本地记录避坑与灵感 | +| ⏱️ 专注时钟 | 番茄钟深度工作计时 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | ❓ FAQ | 搜索 + 展开/收起全部 | diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 9c2b040..5d8ce5e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -7081,3 +7081,249 @@ img { max-width: 100%; display: block; } .tax-stats { grid-template-columns: 1fr 1fr 1fr; } .onboard-card { bottom: 90px; } } + +/* ===== Power Plug ===== */ +.plug-card { + max-width: 720px; + margin: 0 auto; + padding: 28px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-xl); + backdrop-filter: blur(20px); +} + +.plug-hero { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.plug-city { font-size: 1.3rem; font-weight: 700; } + +.plug-types { display: flex; gap: 8px; flex-wrap: wrap; } + +.plug-type { + padding: 6px 12px; + background: rgba(78, 205, 196, 0.12); + color: var(--accent-3); + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; +} + +.plug-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; + margin-bottom: 16px; +} + +.plug-grid div { + padding: 14px; + background: rgba(255,255,255,0.03); + border-radius: var(--radius-md); +} + +.plug-grid small { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.plug-grid strong { font-size: 0.95rem; } + +.plug-tip { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.5; +} + +/* ===== Emergency ===== */ +.emergency-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 14px; +} + +.emergency-card { + display: flex; + gap: 14px; + padding: 18px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-lg); + backdrop-filter: blur(16px); +} + +.emergency-emoji { font-size: 1.8rem; } +.emergency-card h4 { font-size: 0.9rem; margin-bottom: 4px; color: var(--text-secondary); } +.emergency-value { font-size: 1.1rem; display: block; margin-bottom: 4px; } +.emergency-card p { font-size: 0.8rem; color: var(--text-secondary); margin: 0; } + +.emergency-disclaimer { + text-align: center; + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 16px; +} + +/* ===== City Notes ===== */ +.notes-card { + display: grid; + grid-template-columns: 220px 1fr; + gap: 20px; + padding: 24px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-xl); + backdrop-filter: blur(20px); + min-height: 360px; +} + +.notes-sidebar { + display: flex; + flex-direction: column; + gap: 6px; + border-right: 1px solid rgba(255,255,255,0.06); + padding-right: 12px; +} + +.notes-city { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius-md); + color: var(--text-primary); + cursor: pointer; + font-size: 0.9rem; + text-align: left; +} + +.notes-city:hover, +.notes-city.active { + background: rgba(78, 205, 196, 0.1); + border-color: rgba(78, 205, 196, 0.25); +} + +.notes-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-3); +} + +.notes-count { + margin-top: auto; + font-size: 0.75rem; + color: var(--text-secondary); + padding: 8px 4px 0; +} + +.notes-editor h3 { font-size: 1.15rem; margin-bottom: 12px; } + +.notes-textarea { + width: 100%; + padding: 14px; + background: rgba(255,255,255,0.03); + border: var(--border-glass); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.9rem; + font-family: inherit; + resize: vertical; + line-height: 1.55; + margin-bottom: 12px; +} + +.notes-textarea:focus { + outline: none; + border-color: rgba(78, 205, 196, 0.45); +} + +.notes-actions { display: flex; gap: 10px; } + +/* ===== Focus Timer ===== */ +.focus-card { + max-width: 420px; + margin: 0 auto; + padding: 36px 28px; + text-align: center; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-xl); + backdrop-filter: blur(20px); +} + +.focus-ring { + width: 180px; + height: 180px; + border-radius: 50%; + margin: 0 auto 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.focus-ring-inner { + width: 148px; + height: 148px; + border-radius: 50%; + background: var(--bg-card); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.focus-ring-inner strong { + font-size: 2.4rem; + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} + +.focus-ring-inner small { + color: var(--text-secondary); + font-size: 0.85rem; + margin-top: 4px; +} + +.focus-presets { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 18px; +} + +.focus-actions { + display: flex; + justify-content: center; + gap: 10px; +} + +.focus-done { + margin-top: 14px; + color: var(--accent-3); + font-weight: 600; +} + +@media (max-width: 768px) { + .plug-grid { grid-template-columns: 1fr; } + .notes-card { grid-template-columns: 1fr; } + .notes-sidebar { + flex-direction: row; + flex-wrap: wrap; + border-right: none; + border-bottom: 1px solid rgba(255,255,255,0.06); + padding-right: 0; + padding-bottom: 12px; + } + .notes-count { width: 100%; margin-top: 4px; } +} diff --git a/frontend/src/components/CityNotes.tsx b/frontend/src/components/CityNotes.tsx new file mode 100644 index 0000000..0497600 --- /dev/null +++ b/frontend/src/components/CityNotes.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useToast } from "@/lib/toast"; +import type { Destination } from "@/lib/types"; + +const STORAGE_KEY = "nomadro-city-notes"; + +interface Props { + destinations: Destination[]; +} + +export default function CityNotes({ destinations }: Props) { + const { toast } = useToast(); + const [slug, setSlug] = useState(destinations[0]?.slug || "bali"); + const [notes, setNotes] = useState>({}); + const [text, setText] = useState(""); + const [ready, setReady] = useState(false); + const dest = destinations.find((d) => d.slug === slug); + + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) setNotes(JSON.parse(raw)); + } catch { /* ignore */ } + setReady(true); + }, []); + + useEffect(() => { + if (!ready) return; + setText(notes[slug] || ""); + }, [slug, notes, ready]); + + const save = () => { + const next = { ...notes, [slug]: text }; + setNotes(next); + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + toast(`${dest?.emoji || ""} 笔记已保存`); + }; + + const clear = () => { + const next = { ...notes }; + delete next[slug]; + setNotes(next); + setText(""); + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + toast("已清空该城笔记", "info"); + }; + + const filled = Object.keys(notes).filter((k) => notes[k]?.trim()).length; + + return ( +
+
+
+ 📝 NOTES +

城市旅居笔记

+

记录咖啡馆坐标、房东微信、避坑心得——只存在你的浏览器里

+
+ +
+
+ {destinations.map((d) => ( + + ))} +

已记录 {filled} 座城市

+
+ +
+

{dest?.emoji} {dest?.name}

+