From dd1d63fe9df4aa4bd70c4db62a043081c9d7e208 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 08:40:55 +0000 Subject: [PATCH 1/4] Initial plan From 3e0066655f16352f3bc55370822714352af1fd91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:03:15 +0000 Subject: [PATCH 2/4] Set up Nuxt 3 project structure and core components Co-authored-by: wesleyel <48174882+wesleyel@users.noreply.github.com> --- assets/css/main.css | 209 +++++++++++++++++++++++++++++ components/AppFooter.vue | 24 ++++ components/AppHeader.vue | 52 +++++++ components/LanguageSwitcher.vue | 38 ++++++ data/demo.ts | 108 +++++++++++++++ layouts/default.vue | 31 +++++ locales/en.json | 200 +++++++++++++++++++++++++++ locales/zh.json | 200 +++++++++++++++++++++++++++ nuxt.config.ts | 97 +++++++++++++ package.json | 68 ++++------ pages/index.vue | 116 ++++++++++++++++ server/api/bookmarks/[mark].get.ts | 44 ++++++ server/api/bookmarks/add.ts | 183 +++++++++++++++++++++++++ types/index.ts | 37 +++++ utils/index.ts | 67 +++++++++ utils/schema.ts | 22 +++ 16 files changed, 1454 insertions(+), 42 deletions(-) create mode 100644 assets/css/main.css create mode 100644 components/AppFooter.vue create mode 100644 components/AppHeader.vue create mode 100644 components/LanguageSwitcher.vue create mode 100644 data/demo.ts create mode 100644 layouts/default.vue create mode 100644 locales/en.json create mode 100644 locales/zh.json create mode 100644 nuxt.config.ts create mode 100644 pages/index.vue create mode 100644 server/api/bookmarks/[mark].get.ts create mode 100644 server/api/bookmarks/add.ts create mode 100644 types/index.ts create mode 100644 utils/index.ts create mode 100644 utils/schema.ts diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..b58c4d4 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,209 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 84% 4.9%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 84% 4.9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.animate-fadeIn { + animation: fadeIn 0.6s ease-out forwards; +} + +.animate-fadeUp { + animation: fadeUp 0.6s ease-out forwards; +} + +.animate-scaleIn { + animation: scaleIn 0.6s ease-out forwards; +} + +.animate-slideIn { + animation: slideIn 0.6s ease-out forwards; +} + +/* Animation delays */ +.animation-delay-200 { + animation-delay: 200ms; +} + +.animation-delay-400 { + animation-delay: 400ms; +} + +.animation-delay-600 { + animation-delay: 600ms; +} + +.animation-delay-700 { + animation-delay: 700ms; +} + +.animation-delay-800 { + animation-delay: 800ms; +} + +.animation-delay-900 { + animation-delay: 900ms; +} + +/* Stagger animations */ +.stagger-container { + --stagger-delay: 100ms; +} + +.stagger-item { + opacity: 0; + animation: fadeIn 0.6s ease-out forwards; +} + +.stagger-item:nth-child(1) { animation-delay: calc(1 * var(--stagger-delay)); } +.stagger-item:nth-child(2) { animation-delay: calc(2 * var(--stagger-delay)); } +.stagger-item:nth-child(3) { animation-delay: calc(3 * var(--stagger-delay)); } +.stagger-item:nth-child(4) { animation-delay: calc(4 * var(--stagger-delay)); } +.stagger-item:nth-child(5) { animation-delay: calc(5 * var(--stagger-delay)); } +.stagger-item:nth-child(6) { animation-delay: calc(6 * var(--stagger-delay)); } +.stagger-item:nth-child(7) { animation-delay: calc(7 * var(--stagger-delay)); } +.stagger-item:nth-child(8) { animation-delay: calc(8 * var(--stagger-delay)); } +.stagger-item:nth-child(9) { animation-delay: calc(9 * var(--stagger-delay)); } + +/* Interactive elements */ +.hover-scale { + transition: transform 0.2s ease-in-out; +} + +.hover-scale:hover { + transform: scale(1.05); +} + +.active-scale:active { + transform: scale(0.95); +} + +/* Feature card animations */ +.feature-card { + transition: all 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-4px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +/* Delay classes for animations */ +.delay-100 { animation-delay: 100ms; } +.delay-200 { animation-delay: 200ms; } +.delay-300 { animation-delay: 300ms; } +.delay-400 { animation-delay: 400ms; } +.delay-500 { animation-delay: 500ms; } +.delay-600 { animation-delay: 600ms; } +.delay-700 { animation-delay: 700ms; } +.delay-800 { animation-delay: 800ms; } +.delay-900 { animation-delay: 900ms; } \ No newline at end of file diff --git a/components/AppFooter.vue b/components/AppFooter.vue new file mode 100644 index 0000000..541e12c --- /dev/null +++ b/components/AppFooter.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/components/AppHeader.vue b/components/AppHeader.vue new file mode 100644 index 0000000..93398c5 --- /dev/null +++ b/components/AppHeader.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/components/LanguageSwitcher.vue b/components/LanguageSwitcher.vue new file mode 100644 index 0000000..f954897 --- /dev/null +++ b/components/LanguageSwitcher.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/data/demo.ts b/data/demo.ts new file mode 100644 index 0000000..5057e1b --- /dev/null +++ b/data/demo.ts @@ -0,0 +1,108 @@ +import type { BookmarksData } from "~/types" + +// 演示用的书签数据 +export const DEMO_BOOKMARKS_DATA: BookmarksData = { + mark: "demo", + bookmarks: [ + { + uuid: "4d283ee2-1c03-4d39-89e4-910989028ae1", + url: "https://chat.openai.com", + title: "ChatGPT", + favicon: "https://chat.openai.com/favicon.ico", + createdAt: new Date(2023, 0, 1).toISOString(), + modifiedAt: new Date(2023, 0, 1).toISOString(), + category: "AI工具", + description: "OpenAI开发的对话式人工智能助手", + }, + { + uuid: "62e24d2a-a1ab-49f3-899c-6b9212077605", + url: "https://github.com", + title: "GitHub", + favicon: "https://github.com/favicon.ico", + createdAt: new Date(2023, 0, 2).toISOString(), + modifiedAt: new Date(2023, 0, 2).toISOString(), + category: "开发工具", + description: "全球最大的代码托管平台", + }, + { + uuid: "67d1a74c-5f5d-4734-b22b-823848fea115", + url: "https://reactjs.org", + title: "React", + favicon: "https://reactjs.org/favicon.ico", + createdAt: new Date(2023, 0, 3).toISOString(), + modifiedAt: new Date(2023, 0, 3).toISOString(), + category: "开发工具", + description: "用于构建用户界面的JavaScript库", + }, + { + uuid: "e1cd6e04-0f83-41cd-a99b-4d4bc26b015f", + url: "https://nextjs.org", + title: "Next.js", + favicon: "https://nextjs.org/favicon.ico", + createdAt: new Date(2023, 0, 4).toISOString(), + modifiedAt: new Date(2023, 0, 4).toISOString(), + category: "开发工具", + description: "React框架,用于生产环境的应用", + }, + { + uuid: "06acb5e2-4373-4aaf-900d-f4e740267501", + url: "https://www.coursera.org", + title: "Coursera", + favicon: "https://www.coursera.org/favicon.ico", + createdAt: new Date(2023, 0, 5).toISOString(), + modifiedAt: new Date(2023, 0, 5).toISOString(), + category: "学习资源", + description: "提供来自世界各地大学的在线课程", + }, + { + uuid: "f36a531c-10e3-42b3-953c-52ac74668cbf", + url: "https://www.figma.com", + title: "Figma", + favicon: "https://www.figma.com/favicon.ico", + createdAt: new Date(2023, 0, 6).toISOString(), + modifiedAt: new Date(2023, 0, 6).toISOString(), + category: "设计资源", + description: "基于浏览器的界面设计工具", + }, + { + uuid: "8db6a3e1-a4ec-4a7c-94e5-3524b627cdc6", + url: "https://news.ycombinator.com", + title: "Hacker News", + favicon: "https://news.ycombinator.com/favicon.ico", + createdAt: new Date(2023, 0, 7).toISOString(), + modifiedAt: new Date(2023, 0, 7).toISOString(), + category: "新闻资讯", + description: "专注于计算机科学和创业的社交新闻网站", + }, + { + uuid: "276b97ba-1596-4553-b4a1-014b92d83d8f", + url: "https://claude.ai", + title: "Claude AI", + favicon: "https://claude.ai/favicon.ico", + createdAt: new Date(2023, 0, 8).toISOString(), + modifiedAt: new Date(2023, 0, 8).toISOString(), + category: "AI工具", + description: "Anthropic开发的AI助手", + }, + { + uuid: "b063e870-a9e6-4fc1-8f17-f425e0e9b346", + url: "https://www.midjourney.com", + title: "Midjourney", + favicon: "https://www.midjourney.com/favicon.ico", + createdAt: new Date(2023, 0, 9).toISOString(), + modifiedAt: new Date(2023, 0, 9).toISOString(), + category: "AI工具", + description: "AI图像生成工具", + }, + { + uuid: "289309e8-1728-42cd-8a1a-5ba4216ba60b", + url: "https://tailwindcss.com", + title: "Tailwind CSS", + favicon: "https://tailwindcss.com/favicon.ico", + createdAt: new Date(2023, 0, 10).toISOString(), + modifiedAt: new Date(2023, 0, 10).toISOString(), + category: "开发工具", + description: "功能类优先的CSS框架", + }, + ], +} \ No newline at end of file diff --git a/layouts/default.vue b/layouts/default.vue new file mode 100644 index 0000000..e05816d --- /dev/null +++ b/layouts/default.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..ea94c90 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,200 @@ +{ + "HomePage": { + "quickstart": "Quick Start", + "title": "Your Universal Bookmark Manager", + "description": "Save and organize your bookmarks with one click. Access them anywhere, anytime.", + "features": { + "save": { + "title": "One-Click Save", + "desc": "Save any webpage to your collection with a simple drag and drop installation." + }, + "categorize": { + "title": "Smart Categories", + "desc": "Automatically categorize your bookmarks for better organization." + }, + "access": { + "title": "Access Anywhere", + "desc": "Sync across devices, access your bookmarks anywhere." + }, + "share": { + "title": "Share & Collaborate", + "desc": "Easily share bookmark collections and collaborate with others." + } + } + }, + "NotFoundPage": { + "title": "404: This page could not be found.", + "errorCode": "404", + "description": "This page could not be found." + }, + "DocPage": { + "title": "Cloudmark Quick Start", + "description": "Cross-device, cross-browser cloud bookmark management tool", + "loading": "Loading...", + "intro": { + "title": "What is Cloudmark?", + "description": "Cloudmark is a cloud-based bookmark collection tool that allows you to save and organize web bookmarks with one click and access them from any device.", + "features": { + "title": "Key Features", + "oneClick": "One-click saving of current webpage", + "cloud": "Cloud storage for cross-device access", + "categories": "Organize and filter bookmarks by categories", + "custom": "Customizable bookmark collections", + "sorting": "Multiple sorting and layout options", + "multilingual": "Multilingual interface support" + } + }, + "setup": { + "title": "Set Up Your Bookmarklet", + "markName": { + "title": "Set Your Bookmark Name", + "placeholder": "Name your bookmark ID", + "description": "This name will be used for your bookmark collection and can only contain letters, numbers, underscores, and hyphens.", + "button": "Generate Random" + }, + "bookmarklet": { + "title": "Install Bookmark Tool", + "saveButton": "Save to {mark}", + "openButton": "Open {mark} Bookmarks", + "dragTip": "Drag to bookmark bar", + "description": "Drag the button above to your browser's bookmark bar to save the current webpage anytime." + }, + "code": { + "title": "View Bookmarklet Code", + "copied": "Copied" + } + }, + "usage": { + "title": "How to Use Cloudmark", + "steps": { + "setup": { + "title": "Set Up Your Bookmarklet", + "description": "Follow the instructions above to create your bookmark collection name and add the bookmarklet to your browser's bookmark bar. Each bookmark collection name corresponds to a separate bookmark collection." + }, + "save": { + "title": "Save Webpages to Your Collection", + "description": "When browsing a webpage, click the Cloudmark bookmarklet in your bookmark bar to open the bookmark addition page. Fill in the category and description (optional), then save." + }, + "access": { + "title": "Access Your Bookmark Collection", + "description": "Access {baseUrl}/{mark} to view and manage all your saved bookmarks. You can access this URL from any device or browser." + }, + "manage": { + "title": "Organize and Manage Your Bookmarks", + "description": "You can edit or delete bookmarks, change category views, sort by different criteria, and use category filters to quickly find the bookmarks you need." + } + } + }, + "demo": { + "title": "Want to Experience Cloudmark?", + "description": "Try all the features of Cloudmark without creating a bookmark collection", + "button": "View Demo" + }, + "navigation": { + "intro": "Introduction", + "setup": "Setup", + "usage": "Usage", + "demo": "Demo" + } + }, + "Navigation": { + "quickstart": "Quick Start", + "github": "GitHub", + "switchLanguage": "Switch Language" + }, + "BookmarksPage": { + "title": "Bookmarks", + "collection": "Collection {mark}", + "addBookmark": "Add Bookmark", + "noBookmarks": "No bookmarks yet", + "addFirstBookmark": "Add your first bookmark", + "loading": "Loading bookmarks...", + "bookmarkletTip": "Drag this button to your bookmarks bar to save pages with one click", + "saveButton": "Save to {mark}", + "dragTip": "Drag to bookmarks bar", + "demoMode": "Demo Mode", + "demoDescription": "This is a demo collection. You can freely add, edit, and delete bookmarks. All changes will only affect your local view and won't be saved to the server.", + "createOwn": "Create Your Own", + "success": "Success", + "error": "Error", + "warning": "Warning", + "info": "Information" + }, + "Components": { + "MarkInput": { + "label": "Bookmark Mark", + "placeholder": "Enter custom mark", + "randomButton": "Random", + "description": "The mark will be used to distinguish different bookmark collections. It's recommended to use meaningful phrases. Current mark: " + }, + "BookmarkCard": { + "visit": "Visit", + "edit": "Edit", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this bookmark?" + }, + "BookmarkDialog": { + "addTitle": "Add Bookmark", + "addDescription": "Add a new bookmark to collection {mark}", + "editTitle": "Edit Bookmark", + "editDescription": "Edit bookmark information", + "deleteTitle": "Delete Bookmark", + "deleteDescription": "Are you sure you want to delete bookmark \"{title}\"?", + "deleteConfirmation": "This action cannot be undone. The bookmark will be permanently removed from your collection.", + "url": "URL", + "urlPlaceholder": "Enter bookmark URL", + "urlDescription": "The URL of the webpage you want to bookmark", + "title": "Title", + "titlePlaceholder": "Enter bookmark title", + "titleDescription": "A descriptive title for your bookmark", + "description": "Description", + "descriptionPlaceholder": "Enter bookmark description (optional)", + "descriptionDescription": "Add some notes about this bookmark", + "category": "Category", + "categoryPlaceholder": "Select a category", + "categoryDescription": "Choose a category to organize your bookmarks", + "newCategory": "New Category", + "newCategoryPlaceholder": "Enter new category name", + "existingCategory": "Existing Categories", + "backToCategories": "Back to categories", + "cancel": "Cancel", + "reset": "Reset", + "addButton": "Add Bookmark", + "updateButton": "Update Bookmark", + "deleteButton": "Delete Bookmark", + "adding": "Adding...", + "updating": "Updating...", + "deleting": "Deleting...", + "addSuccess": "Bookmark added successfully", + "updateSuccess": "Bookmark updated successfully", + "deleteSuccess": "Bookmark deleted successfully", + "errors": { + "urlRequired": "URL is required", + "invalidUrl": "Please enter a valid URL", + "categoryRequired": "Category is required", + "addFailed": "Failed to add bookmark", + "updateFailed": "Failed to update bookmark", + "deleteFailed": "Failed to delete bookmark" + }, + "addBookmark": "Add Bookmark", + "delete": "Delete bookmark", + "edit": "Edit bookmark" + }, + "FloatingNav": { + "title": "Categories", + "expandNav": "Expand Navigation", + "collapseNav": "Collapse Navigation", + "jumpToCategory": "Jump to {category}" + } + }, + "Notifications": { + "success": "Success", + "error": "Error", + "warning": "Warning", + "info": "Information", + "bookmarkAdded": "Bookmark added successfully", + "bookmarkExists": "Bookmark with this URL already exists", + "urlRequired": "URL is required", + "processingError": "Error processing bookmark" + } +} diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..c954e6e --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,200 @@ +{ + "HomePage": { + "quickstart": "快速入门", + "title": "你的通用书签管理器", + "description": "一键保存和整理书签。随时随地访问。", + "features": { + "save": { + "title": "一键收藏", + "desc": "通过简单的拖拽安装,一键保存任何网页到您的收藏夹。" + }, + "categorize": { + "title": "智能分类", + "desc": "自动对书签进行分类,让您的收藏更有条理。" + }, + "access": { + "title": "随处访问", + "desc": "跨设备同步,随时随地访问您的书签收藏。" + }, + "share": { + "title": "分享协作", + "desc": "轻松与他人分享书签集合,协同整理资源。" + } + } + }, + "NotFoundPage": { + "title": "404: 无法找到此页面。", + "errorCode": "404", + "description": "无法找到此页面。" + }, + "DocPage": { + "title": "Cloudmark 快速入门", + "description": "跨设备、跨浏览器的云端书签管理工具", + "loading": "加载中...", + "intro": { + "title": "什么是 Cloudmark?", + "description": "Cloudmark 是一个基于云的书签收集工具,允许您一键保存和组织网页书签,并从任何设备访问。", + "features": { + "title": "主要特点", + "oneClick": "一键保存当前浏览页面", + "cloud": "云端存储确保跨设备访问", + "categories": "按分类整理和筛选书签", + "custom": "可自定义的书签集合", + "sorting": "支持多种排序和布局视图", + "multilingual": "支持多语言界面" + } + }, + "setup": { + "title": "设置您的 Bookmarklet", + "markName": { + "title": "设置书签的名称", + "placeholder": "为您的书签 ID 取个名字", + "description": "此名称将用于您的书签集合,只能包含字母、数字、下划线和连字符。", + "button": "随机生成" + }, + "bookmarklet": { + "title": "安装书签工具", + "saveButton": "保存到 {mark}", + "openButton": "打开 {mark} 书签", + "dragTip": "拖动到书签栏", + "description": "将上面的按钮拖动到您的浏览器书签栏,以便随时保存当前浏览的网页。" + }, + "code": { + "title": "查看 Bookmarklet 代码", + "copied": "已复制" + } + }, + "usage": { + "title": "如何使用 Cloudmark", + "steps": { + "setup": { + "title": "设置您的 Bookmarklet", + "description": "按照上面的说明创建您的书签集合名称并将 bookmarklet 添加到您的浏览器书签栏中。每个书签集合名称对应一个独立的书签集合。" + }, + "save": { + "title": "保存网页到您的集合", + "description": "当您浏览网页时,点击书签栏中的 Cloudmark bookmarklet,会打开添加书签的页面。填写分类和描述(可选),然后保存即可。" + }, + "access": { + "title": "访问您的书签集合", + "description": "通过访问 {baseUrl}/{mark} 可以查看和管理您保存的所有书签。您可以在任何设备和浏览器上访问这个 URL。" + }, + "manage": { + "title": "整理和管理您的书签", + "description": "您可以编辑或删除书签,更改分类视图,按不同条件排序,以及使用分类筛选器快速找到您需要的书签。" + } + } + }, + "demo": { + "title": "想要体验 Cloudmark?", + "description": "不需要创建书签集合,直接体验 Cloudmark 的全部功能", + "button": "查看演示" + }, + "navigation": { + "intro": "简介", + "setup": "设置", + "usage": "使用方法", + "demo": "演示" + } + }, + "Navigation": { + "quickstart": "快速入门", + "github": "GitHub", + "switchLanguage": "切换语言" + }, + "BookmarksPage": { + "title": "书签收藏", + "collection": "收藏集 {mark}", + "addBookmark": "添加书签", + "noBookmarks": "暂无书签", + "addFirstBookmark": "添加第一个书签", + "loading": "正在加载书签...", + "bookmarkletTip": "拖拽此按钮到书签栏,一键保存网页到此收藏", + "saveButton": "保存到 {mark}", + "dragTip": "拖拽到书签栏", + "demoMode": "演示模式", + "demoDescription": "这是一个演示集合,您可以自由添加、编辑和删除书签,所有操作仅在本地生效,不会保存到服务器。", + "createOwn": "创建自己的收藏", + "success": "成功", + "error": "错误", + "warning": "警告", + "info": "信息" + }, + "Components": { + "MarkInput": { + "label": "书签标记", + "placeholder": "输入自定义标记", + "randomButton": "随机生成", + "description": "标记将用于区分不同的书签收藏,建议使用有意义的词组。当前标记:" + }, + "BookmarkCard": { + "visit": "访问", + "edit": "编辑", + "delete": "删除", + "deleteConfirm": "确定要删除这个书签吗?" + }, + "BookmarkDialog": { + "addTitle": "添加书签", + "addDescription": "添加新书签到 {mark} 收藏夹", + "editTitle": "编辑书签", + "editDescription": "编辑书签信息", + "deleteTitle": "删除书签", + "deleteDescription": "确定要删除书签 \"{title}\" 吗?", + "deleteConfirmation": "此操作无法撤销,删除后书签将从您的收藏中永久移除。", + "url": "网址", + "urlPlaceholder": "输入书签网址", + "urlDescription": "您想要收藏的网页地址", + "title": "标题", + "titlePlaceholder": "输入书签标题", + "titleDescription": "为您的书签添加一个描述性的标题", + "description": "描述", + "descriptionPlaceholder": "输入书签描述(可选)", + "descriptionDescription": "添加一些关于这个书签的备注", + "category": "分类", + "categoryPlaceholder": "选择分类", + "categoryDescription": "选择一个分类来组织您的书签", + "newCategory": "新建分类", + "newCategoryPlaceholder": "输入新分类名称", + "existingCategory": "现有分类", + "backToCategories": "返回分类列表", + "cancel": "取消", + "reset": "重置", + "addButton": "添加书签", + "updateButton": "更新书签", + "deleteButton": "删除书签", + "adding": "添加中...", + "updating": "更新中...", + "deleting": "删除中...", + "addSuccess": "书签添加成功", + "updateSuccess": "书签更新成功", + "deleteSuccess": "书签删除成功", + "errors": { + "urlRequired": "请输入网址", + "invalidUrl": "请输入有效的网址", + "categoryRequired": "请选择分类", + "addFailed": "添加书签失败", + "updateFailed": "更新书签失败", + "deleteFailed": "删除书签失败" + }, + "addBookmark": "添加书签", + "delete": "删除书签", + "edit": "编辑" + }, + "FloatingNav": { + "title": "分类导航", + "expandNav": "展开导航", + "collapseNav": "折叠导航", + "jumpToCategory": "跳转到{category}分类" + } + }, + "Notifications": { + "success": "成功", + "error": "错误", + "warning": "警告", + "info": "通知", + "bookmarkAdded": "书签已成功添加", + "bookmarkExists": "该 URL 的书签已存在", + "urlRequired": "URL 是必需的", + "processingError": "处理书签时出错" + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts new file mode 100644 index 0000000..e6dace6 --- /dev/null +++ b/nuxt.config.ts @@ -0,0 +1,97 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + devtools: { enabled: true }, + + // Modules + modules: [ + '@nuxtjs/tailwindcss', + '@nuxtjs/i18n', + '@vueuse/nuxt', + '@nuxt/icon', + '@nuxtjs/cloudflare' + ], + + // CSS + css: ['~/assets/css/main.css'], + + // TypeScript configuration + typescript: { + strict: true, + typeCheck: true + }, + + // Internationalization + i18n: { + strategy: 'prefix_except_default', + defaultLocale: 'en', + locales: [ + { code: 'en', name: 'English', file: 'en.json' }, + { code: 'zh', name: '中文', file: 'zh.json' } + ], + langDir: 'locales/', + detectBrowserLanguage: { + useCookie: true, + cookieKey: 'i18n_redirected', + redirectOn: 'root' + } + }, + + // App configuration + app: { + head: { + title: 'Cloudmark - Your Universal Bookmark Manager', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { + name: 'description', + content: 'Save and access your bookmarks from anywhere with Cloudmark, the seamless cloud bookmarking tool for professionals and casual users alike' + }, + { + name: 'keywords', + content: 'cloudmark,cloud mark,bookmarks,book mark manager,bookmark manager,bookmark managers,bookmark organizer,bookmark saver,bookmarks manager,bookmark editor,web tool,productivity' + } + ], + link: [ + { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } + ] + } + }, + + // Runtime config + runtimeConfig: { + public: { + baseUrl: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000' + } + }, + + // Nitro configuration for Cloudflare + nitro: { + preset: 'cloudflare-pages', + experimental: { + wasm: true + } + }, + + // Development server + devServer: { + port: 3000 + }, + + // Build configuration + build: { + transpile: ['lucide-vue-next'] + }, + + // Cloudflare module configuration + cloudflare: { + pages: { + routes: { + exclude: ['/api/*'] + } + } + }, + + // SSR configuration + ssr: true +}) \ No newline at end of file diff --git a/package.json b/package.json index 17a6eff..10bb3ea 100644 --- a/package.json +++ b/package.json @@ -3,64 +3,48 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint", - "preview": "opennextjs-cloudflare && wrangler dev", - "deploy": "opennextjs-cloudflare && wrangler deploy", - "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts", - "analyze": "ANALYZE=true next build" + "dev": "nuxt dev", + "build": "nuxt build", + "start": "nuxt start", + "preview": "nuxt preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "deploy": "nuxt build && wrangler deploy", + "cf-typegen": "wrangler types --env-interface CloudflareEnv types/env.d.ts" }, "dependencies": { - "@hookform/resolvers": "^4.1.3", - "@next/third-parties": "^15.1.7", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.3", + "@headlessui/vue": "^1.7.23", + "@nuxt/icon": "^1.9.0", + "@nuxtjs/cloudflare": "^0.4.0", + "@nuxtjs/i18n": "^9.0.5", + "@nuxtjs/tailwindcss": "^6.13.2", + "@vueform/valibot": "^1.6.0", + "@vueuse/core": "^11.4.0", + "@vueuse/nuxt": "^11.4.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.475.0", - "next": "15.1.6", - "next-intl": "^3.26.5", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.54.2", - "sonner": "^2.0.1", + "lucide-vue-next": "^0.475.0", + "nuxt": "^3.15.1", + "radix-vue": "^1.9.14", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", + "valibot": "^0.42.1", + "vue": "^3.5.14", "zod": "^3.24.2", - "zsa": "^0.6.0", - "zsa-react": "^0.2.3" + "vue-router": "^4.5.0", + "vue-sonner": "^1.2.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250214.0", - "@eslint/eslintrc": "^3", - "@next/bundle-analyzer": "^15.1.6", - "@opennextjs/cloudflare": "^0.5.4", + "@nuxt/eslint": "^0.8.0", "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", "eslint": "^9", - "eslint-config-next": "15.1.6", + "nitropack": "^2.10.4", "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5", - "vercel": "^41.2.0", "wrangler": "^3.109.3" }, - "packageManager": "pnpm@9.14.4", - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild", - "sharp", - "workerd" - ] - } + "packageManager": "npm@10.8.2" } diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..e928364 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/server/api/bookmarks/[mark].get.ts b/server/api/bookmarks/[mark].get.ts new file mode 100644 index 0000000..d792938 --- /dev/null +++ b/server/api/bookmarks/[mark].get.ts @@ -0,0 +1,44 @@ +import type { BookmarksData } from '~/types' +import { isDemoMark } from '~/types' +import { DEMO_BOOKMARKS_DATA } from '~/data/demo' + +export default defineEventHandler(async (event) => { + const mark = getRouterParam(event, 'mark') + + if (!mark) { + throw createError({ + statusCode: 400, + statusMessage: 'Mark parameter is required' + }) + } + + const isDemo = isDemoMark(mark) + if (isDemo) { + return DEMO_BOOKMARKS_DATA + } + + try { + // For Cloudflare KV storage + const env = event.context.cloudflare?.env + if (!env?.cloudmark) { + throw createError({ + statusCode: 500, + statusMessage: 'KV storage not available' + }) + } + + const bookmarksData = await env.cloudmark.get(mark, 'json') + + if (!bookmarksData) { + return null + } + + return bookmarksData + } catch (error) { + console.error('Error fetching bookmarks:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fetch bookmarks' + }) + } +}) \ No newline at end of file diff --git a/server/api/bookmarks/add.ts b/server/api/bookmarks/add.ts new file mode 100644 index 0000000..2d37f2e --- /dev/null +++ b/server/api/bookmarks/add.ts @@ -0,0 +1,183 @@ +import type { BookmarkInstance, BookmarksData } from '~/types' +import { isDemoMark, defaultCategory } from '~/types' +import { bookmarkSchema } from '~/utils/schema' + +function generateUUID(): string { + return crypto.randomUUID() +} + +function createDefaultBookmarksData(mark: string): BookmarksData { + return { + mark, + bookmarks: [], + } +} + +async function getFavicon(url: string, size: number = 64): Promise { + const domain = new URL(url).hostname.replace("www.", "") + return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size}` +} + +export default defineEventHandler(async (event) => { + const method = getMethod(event) + + if (method === 'GET') { + // Handle bookmarklet GET request + const query = getQuery(event) + const { mark, title = 'Untitled', url } = query + + if (!mark || !url) { + throw createError({ + statusCode: 400, + statusMessage: 'Mark and URL parameters are required' + }) + } + + const formData = { + mark: mark as string, + url: url as string, + title: title as string, + category: defaultCategory + } + + // Validate data + const result = bookmarkSchema.safeParse(formData) + if (!result.success) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid data: ' + result.error.message + }) + } + + const data = result.data + const isDemo = isDemoMark(data.mark) + + if (isDemo) { + throw createError({ + statusCode: 400, + statusMessage: 'Demo mode - cannot add bookmarks' + }) + } + + try { + const env = event.context.cloudflare?.env + if (!env?.cloudmark) { + throw createError({ + statusCode: 500, + statusMessage: 'KV storage not available' + }) + } + + let bookmarksData = await env.cloudmark.get(data.mark, 'json') + if (!bookmarksData) { + bookmarksData = createDefaultBookmarksData(data.mark) + } + + // Check if bookmark already exists + if (bookmarksData.bookmarks.find((b) => b.url === data.url)) { + throw createError({ + statusCode: 409, + statusMessage: `Bookmark ${data.title} (${data.url}) already exists` + }) + } + + const uuid = generateUUID() + const favicon = await getFavicon(data.url) + const newBookmark: BookmarkInstance = { + uuid, + url: data.url, + title: data.title, + description: data.description || '', + category: data.category, + favicon, + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + } + + bookmarksData.bookmarks.push(newBookmark) + await env.cloudmark.put(data.mark, JSON.stringify(bookmarksData)) + + // Redirect back to bookmark page with success message + return sendRedirect(event, `/${data.mark}?status=success&message=${encodeURIComponent('Bookmark added successfully')}`) + } catch (error: any) { + console.error('Error adding bookmark:', error) + return sendRedirect(event, `/${data.mark}?status=error&message=${encodeURIComponent(error.message || 'Failed to add bookmark')}`) + } + } + + if (method === 'POST') { + // Handle form POST request + const body = await readBody(event) + + // Validate data + const result = bookmarkSchema.safeParse(body) + if (!result.success) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid data: ' + result.error.message + }) + } + + const data = result.data + const isDemo = isDemoMark(data.mark) + + if (isDemo) { + throw createError({ + statusCode: 400, + statusMessage: 'Demo mode - cannot add bookmarks' + }) + } + + try { + const env = event.context.cloudflare?.env + if (!env?.cloudmark) { + throw createError({ + statusCode: 500, + statusMessage: 'KV storage not available' + }) + } + + let bookmarksData = await env.cloudmark.get(data.mark, 'json') + if (!bookmarksData) { + bookmarksData = createDefaultBookmarksData(data.mark) + } + + // Check if bookmark already exists + if (bookmarksData.bookmarks.find((b) => b.url === data.url)) { + throw createError({ + statusCode: 409, + statusMessage: `Bookmark ${data.title} (${data.url}) already exists` + }) + } + + const uuid = generateUUID() + const favicon = await getFavicon(data.url) + const newBookmark: BookmarkInstance = { + uuid, + url: data.url, + title: data.title, + description: data.description || '', + category: data.category, + favicon, + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + } + + bookmarksData.bookmarks.push(newBookmark) + await env.cloudmark.put(data.mark, JSON.stringify(bookmarksData)) + + return newBookmark + } catch (error: any) { + console.error('Error adding bookmark:', error) + throw createError({ + statusCode: 500, + statusMessage: error.message || 'Failed to add bookmark' + }) + } + } + + throw createError({ + statusCode: 405, + statusMessage: 'Method not allowed' + }) +}) \ No newline at end of file diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..7300fa0 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,37 @@ +export const defaultMark = "demo"; +export const defaultCategory = "General"; + +export interface BookmarkInstance { + uuid: string; + url: string; + title: string; + description: string; + category: string; + favicon: string; + createdAt: string; + modifiedAt: string; +} + +export interface BookmarksData { + mark: string; + bookmarks: BookmarkInstance[]; +} + +export function isDemoMark(mark: string): boolean { + return mark === defaultMark; +} + +export interface Toast { + title: string; + description?: string; + variant?: 'default' | 'success' | 'error' | 'warning' | 'info'; +} + +export interface BookmarkFormData { + mark: string; + url: string; + title: string; + description?: string; + category: string; + uuid?: string; +} \ No newline at end of file diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..cda5440 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,67 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" +import type { BookmarksData } from "~/types" +import { defaultCategory } from "~/types" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function getBaseUrl(): string { + const config = useRuntimeConfig() + return config.public.baseUrl || 'http://localhost:3000' +} + +export function getCategories(bookmarksData: BookmarksData | null): string[] { + if (!bookmarksData || !bookmarksData.bookmarks) { + return [defaultCategory] + } + + const uniqueCategories = [ + ...new Set(bookmarksData.bookmarks.map((bookmark) => bookmark.category)) + ] + + if (!uniqueCategories.includes(defaultCategory)) { + return [defaultCategory, ...uniqueCategories] + } + + return [ + defaultCategory, + ...uniqueCategories.filter((category) => category !== defaultCategory) + ] +} + +export function getDomain(url: string): string { + return new URL(url).hostname.replace("www.", "") +} + +export function generateRandomMark(): string { + const adjectives = [ + "vacuous", + "tearful", + "faint", + "jumbled", + "wandering", + "mature", + "savory", + "mighty", + "disgusted", + "abstracted", + "telling", + ] + const nouns = [ + "person", + "inspector", + "significance", + "chapter", + "reputation", + "outcome", + "association", + "failure", + "population", + "wealth", + "bird", + ] + const randomNum = Math.floor(Math.random() * 10000) + return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}-${randomNum}` +} \ No newline at end of file diff --git a/utils/schema.ts b/utils/schema.ts new file mode 100644 index 0000000..bc3e43c --- /dev/null +++ b/utils/schema.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +export const bookmarkSchema = z.object({ + mark: z.string().min(1, 'Mark is required'), + url: z.string().url('Invalid URL'), + title: z.string().min(1, 'Title is required'), + description: z.string().optional(), + category: z.string().min(1, 'Category is required'), +}) + +export const updateBookmarkSchema = bookmarkSchema.extend({ + uuid: z.string().min(1, 'UUID is required'), +}) + +export const deleteBookmarkSchema = z.object({ + mark: z.string().min(1, 'Mark is required'), + uuid: z.string().min(1, 'UUID is required'), +}) + +export type BookmarkFormData = z.infer +export type UpdateBookmarkFormData = z.infer +export type DeleteBookmarkFormData = z.infer \ No newline at end of file From 52129f0356d8e171a1e9b21a07f43ecc413a479b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:10:49 +0000 Subject: [PATCH 3/4] Complete Vue components migration and add missing pages Co-authored-by: wesleyel <48174882+wesleyel@users.noreply.github.com> --- components/BookmarkCard.vue | 130 ++++++++++++++++++++ components/BookmarkUI.vue | 182 ++++++++++++++++++++++++++++ components/BookmarkletButton.vue | 60 +++++++++ components/DemoBanner.vue | 45 +++++++ components/DialogAdd.vue | 198 ++++++++++++++++++++++++++++++ components/DialogDelete.vue | 123 +++++++++++++++++++ components/DialogEdit.vue | 190 +++++++++++++++++++++++++++++ components/FloatingNav.vue | 38 ++++++ pages/[mark].vue | 52 ++++++++ pages/doc.vue | 202 +++++++++++++++++++++++++++++++ plugins/toast.client.ts | 14 +++ server/api/bookmarks/delete.ts | 72 +++++++++++ server/api/bookmarks/update.ts | 81 +++++++++++++ 13 files changed, 1387 insertions(+) create mode 100644 components/BookmarkCard.vue create mode 100644 components/BookmarkUI.vue create mode 100644 components/BookmarkletButton.vue create mode 100644 components/DemoBanner.vue create mode 100644 components/DialogAdd.vue create mode 100644 components/DialogDelete.vue create mode 100644 components/DialogEdit.vue create mode 100644 components/FloatingNav.vue create mode 100644 pages/[mark].vue create mode 100644 pages/doc.vue create mode 100644 plugins/toast.client.ts create mode 100644 server/api/bookmarks/delete.ts create mode 100644 server/api/bookmarks/update.ts diff --git a/components/BookmarkCard.vue b/components/BookmarkCard.vue new file mode 100644 index 0000000..dd5cc47 --- /dev/null +++ b/components/BookmarkCard.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/components/BookmarkUI.vue b/components/BookmarkUI.vue new file mode 100644 index 0000000..e3ef6f2 --- /dev/null +++ b/components/BookmarkUI.vue @@ -0,0 +1,182 @@ + + + \ No newline at end of file diff --git a/components/BookmarkletButton.vue b/components/BookmarkletButton.vue new file mode 100644 index 0000000..f3e2d0e --- /dev/null +++ b/components/BookmarkletButton.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/components/DemoBanner.vue b/components/DemoBanner.vue new file mode 100644 index 0000000..7624c53 --- /dev/null +++ b/components/DemoBanner.vue @@ -0,0 +1,45 @@ + + + \ No newline at end of file diff --git a/components/DialogAdd.vue b/components/DialogAdd.vue new file mode 100644 index 0000000..ab94b71 --- /dev/null +++ b/components/DialogAdd.vue @@ -0,0 +1,198 @@ +