diff --git a/README.md b/README.md index d1e837b..515d00d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,10 @@ docker compose up -d | 📶 网速评级 | 远程办公 WiFi 评级与排行 | | 🎯 启动金目标 | 存款进度追踪,预估出发时间 | | ✨ 今日金句 | 每日游民灵感语录 | +| 😴 时差估算 | 时差跨度与恢复期建议 | +| 🏡 住宿指南 | Villa / Coliving / 公寓方案对比 | +| 🗣️ 生存短语 | 多语言常用语一键复制 | +| 💬 反馈入口 | 浮动反馈表单 | | 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 | | ❓ FAQ | 搜索 + 展开/收起全部 | diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 7b3b242..cef70fc 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -6377,3 +6377,234 @@ img { max-width: 100%; display: block; } grid-template-columns: 1fr; } } + +/* ===== Jet Lag ===== */ +.jetlag-card { + display: grid; + grid-template-columns: 1fr 1.1fr; + gap: 28px; + padding: 28px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-xl); + backdrop-filter: blur(20px); +} + +.jetlag-form { display: flex; flex-direction: column; gap: 16px; } + +.jetlag-result { + padding: 24px; + background: rgba(167, 139, 250, 0.06); + border: 1px solid rgba(167, 139, 250, 0.15); + border-radius: var(--radius-lg); +} + +.jetlag-diff { + text-align: center; + margin-bottom: 20px; +} + +.jetlag-emoji { font-size: 2.5rem; display: block; margin-bottom: 8px; } +.jetlag-diff strong { font-size: 1.8rem; display: block; } +.jetlag-diff small { color: var(--text-secondary); font-size: 0.85rem; } + +.jetlag-recovery { + text-align: center; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(255,255,255,0.06); +} + +.jetlag-recovery span { + display: block; + font-size: 0.8rem; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.jetlag-recovery strong { font-size: 1.5rem; } + +.jetlag-note { + margin-top: 8px; + font-size: 0.8rem; + color: var(--accent-2); +} + +.jetlag-tips { + list-style: none; + padding: 0; + margin: 0; +} + +.jetlag-tips li { + font-size: 0.9rem; + color: var(--text-secondary); + padding: 6px 0; + line-height: 1.45; +} + +/* ===== Housing ===== */ +.housing-cities { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin-bottom: 24px; +} + +.housing-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 12px; +} + +.housing-header h3 { font-size: 1.2rem; } + +.housing-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 18px; +} + +.housing-card { + padding: 22px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-lg); + backdrop-filter: blur(16px); + transition: transform 0.3s; +} + +.housing-card:hover { transform: translateY(-4px); } + +.housing-card-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.housing-emoji { font-size: 1.8rem; } +.housing-price { font-size: 0.85rem; color: var(--accent-2); font-weight: 600; } +.housing-card h4 { font-size: 1.05rem; margin-bottom: 6px; } +.housing-best { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 12px; } + +.housing-pros { display: flex; flex-wrap: wrap; gap: 6px; } +.housing-pro { + font-size: 0.75rem; + padding: 4px 10px; + background: rgba(255,255,255,0.05); + border-radius: 20px; + color: var(--text-secondary); +} + +/* ===== Phrasebook ===== */ +.phrase-card { + padding: 28px; + background: var(--bg-card); + border: var(--border-glass); + border-radius: var(--radius-xl); + backdrop-filter: blur(20px); +} + +.phrase-lang { + text-align: center; + font-weight: 600; + margin-bottom: 20px; + color: var(--accent-3); +} + +.phrase-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.phrase-item { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: 14px 16px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); + border-radius: var(--radius-md); + color: var(--text-primary); + cursor: pointer; + text-align: left; + transition: all 0.25s; +} + +.phrase-item:hover { + border-color: rgba(78, 205, 196, 0.4); + background: rgba(78, 205, 196, 0.08); + transform: translateY(-2px); +} + +.phrase-item strong { font-size: 1rem; } +.phrase-pinyin { font-size: 0.75rem; color: var(--accent-3); } +.phrase-zh { font-size: 0.85rem; color: var(--text-secondary); } + +/* ===== Feedback Widget ===== */ +.feedback-fab { + position: fixed; + bottom: 100px; + right: 24px; + z-index: 8600; + width: 48px; + height: 48px; + border-radius: 50%; + border: var(--border-glass); + background: var(--bg-card); + backdrop-filter: blur(16px); + font-size: 1.3rem; + cursor: pointer; + box-shadow: var(--shadow-card); + transition: transform 0.25s; +} + +.feedback-fab:hover { transform: scale(1.08); } + +.feedback-modal { max-width: 420px; } + +.feedback-body { + padding: 8px 4px 4px; + text-align: center; +} + +.feedback-body h2 { margin: 12px 0 8px; font-size: 1.4rem; } +.feedback-body > p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 20px; } + +.feedback-types { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 14px; + flex-wrap: wrap; +} + +.feedback-textarea { + width: 100%; + padding: 14px; + margin-bottom: 14px; + background: rgba(255,255,255,0.04); + border: var(--border-glass); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.9rem; + resize: vertical; + font-family: inherit; +} + +.feedback-textarea:focus { + outline: none; + border-color: rgba(78, 205, 196, 0.5); +} + +@media (max-width: 768px) { + .jetlag-card { grid-template-columns: 1fr; } + .feedback-fab { bottom: 160px; right: 16px; } +} diff --git a/frontend/src/components/FeedbackWidget.tsx b/frontend/src/components/FeedbackWidget.tsx new file mode 100644 index 0000000..4a54a62 --- /dev/null +++ b/frontend/src/components/FeedbackWidget.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { useToast } from "@/lib/toast"; + +const TYPES = [ + { key: "idea", label: "💡 想法" }, + { key: "bug", label: "🐛 问题" }, + { key: "love", label: "❤️ 喜欢" }, +]; + +export default function FeedbackWidget() { + const { toast } = useToast(); + const [open, setOpen] = useState(false); + const [type, setType] = useState("idea"); + const [msg, setMsg] = useState(""); + const [sending, setSending] = useState(false); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!msg.trim()) return; + setSending(true); + // Persist locally for demo; can wire to API later + try { + const key = "nomadro-feedback"; + const prev = JSON.parse(localStorage.getItem(key) || "[]"); + prev.push({ type, msg: msg.trim(), at: new Date().toISOString() }); + localStorage.setItem(key, JSON.stringify(prev.slice(-50))); + toast("感谢反馈!我们会认真看的 🙏"); + setMsg(""); + setOpen(false); + } finally { + setSending(false); + } + }; + + return ( + <> + + + {open && ( +
e.target === e.currentTarget && setOpen(false)}> +
+ +
+ 💬 FEEDBACK +

给 nomadro 提建议

+

功能想法、体验问题,或只是想说喜欢

+
+
+ {TYPES.map((t) => ( + + ))} +
+