From d3fc3bf348085684420a5e55eefdc194f006095a Mon Sep 17 00:00:00 2001 From: csark0812 Date: Tue, 2 Sep 2025 12:11:28 -0700 Subject: [PATCH 1/5] update env --- .env.example | 13 +++++++++++++ .gitignore | 1 + 2 files changed, 14 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..34ea63c --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Database (production) +DATABASE_URL="postgresql://prod_user:password@host/database" + +# Email Service +INBOUND_API_KEY="your_production_api_key" + +# Auth +BETTER_AUTH_SECRET="your_secure_random_string" +BETTER_AUTH_URL="https://your-domain.com" + +# GitHub +GITHUB_CLIENT_ID="your-client-id" +GITHUB_CLIENT_SECRET="your-client-secret" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 37c2b6f..0742840 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ yarn-error.log* # env files .env* +!.env.example # vercel .vercel From 769a6183faa46d91938f1c0001b10b4433aab6d6 Mon Sep 17 00:00:00 2001 From: csark0812 Date: Tue, 2 Sep 2025 12:59:44 -0700 Subject: [PATCH 2/5] add dashboard to top buttons --- components/auth/user-avatar.tsx | 8 +------- components/header.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/components/auth/user-avatar.tsx b/components/auth/user-avatar.tsx index 6647470..2f68cb2 100644 --- a/components/auth/user-avatar.tsx +++ b/components/auth/user-avatar.tsx @@ -225,13 +225,7 @@ export function UserAvatar() { -
-
- - Dashboard - -
-
+
diff --git a/components/header.tsx b/components/header.tsx index 8150bc4..6378331 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -40,6 +40,14 @@ export function Header() { +
From d7f3d8789928685b869dddbb2ade11c3f5cb01aa Mon Sep 17 00:00:00 2001 From: csark0812 Date: Tue, 2 Sep 2025 13:29:55 -0700 Subject: [PATCH 3/5] Enhance feed and dashboard functionality with search and selection features - Added search functionality to the feed page, allowing users to filter rules by title, content, or author. - Implemented optional search filters in the hot and new API routes for fetching rules. - Introduced rule selection capabilities in the dashboard, enabling users to select multiple rules for actions like adding to lists or deleting. - Enhanced user experience with a dialog for adding selected rules to lists and improved feedback mechanisms for actions taken on rules. --- app/api/feed/hot/route.ts | 24 ++- app/api/feed/new/route.ts | 24 ++- app/dashboard/page.tsx | 310 +++++++++++++++++++++++++++- app/feed/page.tsx | 81 ++++++-- components/dashboard/user-lists.tsx | 6 +- 5 files changed, 411 insertions(+), 34 deletions(-) diff --git a/app/api/feed/hot/route.ts b/app/api/feed/hot/route.ts index c4f488c..f6b76cc 100644 --- a/app/api/feed/hot/route.ts +++ b/app/api/feed/hot/route.ts @@ -1,12 +1,14 @@ import { NextRequest, NextResponse } from "next/server" import { db } from "@/lib/db" import { cursorRule, user } from "@/lib/schema" -import { eq, desc } from "drizzle-orm" +import { eq, desc, or, ilike } from "drizzle-orm" export async function GET(request: NextRequest) { try { - // Fetch most viewed public rules - const rules = await db + const { searchParams } = new URL(request.url) + const searchQuery = searchParams.get('q') + + let query = db .select({ id: cursorRule.id, title: cursorRule.title, @@ -22,6 +24,22 @@ export async function GET(request: NextRequest) { .from(cursorRule) .innerJoin(user, eq(cursorRule.userId, user.id)) .where(eq(cursorRule.isPublic, true)) + + // Add search filter if query is provided + if (searchQuery && searchQuery.trim()) { + const searchTerm = `%${searchQuery.trim()}%` + query = query.where( + or( + eq(cursorRule.isPublic, true), + ilike(cursorRule.title, searchTerm), + ilike(cursorRule.content, searchTerm), + ilike(user.name, searchTerm) + ) + ) + } + + // Fetch most viewed public rules (with optional search) + const rules = await query .orderBy(desc(cursorRule.views), desc(cursorRule.createdAt)) .limit(50) diff --git a/app/api/feed/new/route.ts b/app/api/feed/new/route.ts index adbdf33..0b3754c 100644 --- a/app/api/feed/new/route.ts +++ b/app/api/feed/new/route.ts @@ -1,12 +1,14 @@ import { NextRequest, NextResponse } from "next/server" import { db } from "@/lib/db" import { cursorRule, user } from "@/lib/schema" -import { eq, desc } from "drizzle-orm" +import { eq, desc, or, ilike } from "drizzle-orm" export async function GET(request: NextRequest) { try { - // Fetch newest public rules - const rules = await db + const { searchParams } = new URL(request.url) + const searchQuery = searchParams.get('q') + + let query = db .select({ id: cursorRule.id, title: cursorRule.title, @@ -22,6 +24,22 @@ export async function GET(request: NextRequest) { .from(cursorRule) .innerJoin(user, eq(cursorRule.userId, user.id)) .where(eq(cursorRule.isPublic, true)) + + // Add search filter if query is provided + if (searchQuery && searchQuery.trim()) { + const searchTerm = `%${searchQuery.trim()}%` + query = query.where( + or( + eq(cursorRule.isPublic, true), + ilike(cursorRule.title, searchTerm), + ilike(cursorRule.content, searchTerm), + ilike(user.name, searchTerm) + ) + ) + } + + // Fetch newest public rules (with optional search) + const rules = await query .orderBy(desc(cursorRule.createdAt)) .limit(50) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 9aa5edd..971c6db 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -120,6 +120,10 @@ export default function DashboardPage() { const [nameInput, setNameInput] = useState("") const [showNamePrompt, setShowNamePrompt] = useState(false) const [isUpdatingName, setIsUpdatingName] = useState(false) + const [selectedRules, setSelectedRules] = useState>(new Set()) + const [showListDialog, setShowListDialog] = useState(false) + const [availableLists, setAvailableLists] = useState>([]) + const [isAddingToList, setIsAddingToList] = useState(false) // Helper function to show checkmark feedback const showActionFeedback = (actionKey: string) => { @@ -129,6 +133,109 @@ export default function DashboardPage() { }, 1500) } + // Helper functions for rule selection + const toggleRuleSelection = (ruleId: string) => { + setSelectedRules(prev => { + const newSet = new Set(prev) + if (newSet.has(ruleId)) { + newSet.delete(ruleId) + } else { + newSet.add(ruleId) + } + return newSet + }) + } + + const selectAllRules = () => { + setSelectedRules(new Set(rules.map(rule => rule.id))) + } + + const clearSelection = () => { + setSelectedRules(new Set()) + } + + const isRuleSelected = (ruleId: string) => { + return selectedRules.has(ruleId) + } + + // Fetch available lists for adding rules + const fetchAvailableLists = async () => { + try { + const response = await fetch('/api/lists') + if (response.ok) { + const lists = await response.json() + setAvailableLists(lists.map((list: any) => ({ id: list.id, title: list.title }))) + } + } catch (error) { + console.error('Error fetching lists:', error) + } + } + + // Handle adding selected rules to a list + const handleAddSelectedToList = async (listId: string) => { + if (selectedRules.size === 0) return + + setIsAddingToList(true) + try { + const promises = Array.from(selectedRules).map(ruleId => + fetch(`/api/lists/${listId}/rules`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ruleId }) + }) + ) + + const results = await Promise.allSettled(promises) + const successful = results.filter(result => result.status === 'fulfilled').length + const failed = results.length - successful + + if (successful > 0) { + toast.success(`Added ${successful} rule${successful !== 1 ? 's' : ''} to list!`) + track("Rules Added to List", { count: successful, listId }) + } + if (failed > 0) { + toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (may already be in list)`) + } + + setShowListDialog(false) + clearSelection() + } catch (error) { + console.error('Error adding rules to list:', error) + toast.error('Failed to add rules to list') + } finally { + setIsAddingToList(false) + } + } + + // Handle deleting selected rules + const handleDeleteSelected = async () => { + if (selectedRules.size === 0) return + + try { + const promises = Array.from(selectedRules).map(ruleId => + fetch('/api/cursor-rules', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: ruleId }) + }) + ) + + const results = await Promise.allSettled(promises) + const successful = results.filter(result => result.status === 'fulfilled').length + + if (successful > 0) { + // Remove successfully deleted rules from local state + setRules(prevRules => prevRules.filter(rule => !selectedRules.has(rule.id))) + toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`) + track("Rules Deleted", { count: successful }) + clearSelection() + } + } catch (error) { + console.error('Error deleting rules:', error) + toast.error('Failed to delete rules') + } + } + // Check if user needs to set their name const needsNameUpdate = (user: any) => { if (!user?.name) return true @@ -305,6 +412,7 @@ export default function DashboardPage() { if (session) { fetchRules() + fetchAvailableLists() } }, [session]) @@ -390,9 +498,129 @@ export default function DashboardPage() { {/* User Lists Section */}
-

Your Cursor Rules

-
- {rulesLoading ? "Loading..." : `${rules.length} rule${rules.length !== 1 ? 's' : ''}`} +
+

Your Cursor Rules

+
+ {rulesLoading ? "Loading..." : `${rules.length} rule${rules.length !== 1 ? 's' : ''}`} +
+
+
+ {!rulesLoading && rules.length > 0 && ( + <> + + + 0 ? 'text-gray-400' : 'text-gray-600'}`}> + {selectedRules.size} selected + + + + + + + + + + + Delete Selected Rules + + Are you sure you want to delete {selectedRules.size} rule{selectedRules.size !== 1 ? 's' : ''}? This action cannot be undone. + + + + + + + + + + + + + + + + )}
@@ -429,13 +657,25 @@ export default function DashboardPage() { {/* Left side - Rule info */}
- - compose-3 - - - - - +
@@ -666,6 +906,56 @@ export default function DashboardPage() { )}
+ + {/* List Selection Dialog */} + + + + Add Rules to List + + Select a list to add {selectedRules.size} rule{selectedRules.size !== 1 ? 's' : ''} to. + + +
+ {availableLists.length === 0 ? ( +
+

No lists available. Create a list first.

+
+ ) : ( + availableLists.map((list) => ( + + )) + )} +
+ + + +
+
) } diff --git a/app/feed/page.tsx b/app/feed/page.tsx index 790f1d8..df219ec 100644 --- a/app/feed/page.tsx +++ b/app/feed/page.tsx @@ -1,9 +1,10 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useState, useCallback } from "react" import { Header } from "@/components/header" import { Card } from "@/components/ui/card" -import { Eye, Clock } from "lucide-react" +import { Input } from "@/components/ui/input" +import { Eye, Clock, Search, X } from "lucide-react" import { toast } from "sonner" import { track } from "@vercel/analytics" import { AddToListButton } from "@/components/lists/add-to-list-button" @@ -114,6 +115,8 @@ export default function FeedPage() { const [newRules, setNewRules] = useState([]) const [loading, setLoading] = useState(true) const [actionStates, setActionStates] = useState<{[key: string]: boolean}>({}) + const [searchQuery, setSearchQuery] = useState('') + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') // Helper function to show checkmark feedback const showActionFeedback = (actionKey: string) => { @@ -132,14 +135,31 @@ export default function FeedPage() { ) + // Debounce search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery) + }, 300) + + return () => clearTimeout(timer) + }, [searchQuery]) + useEffect(() => { const fetchRules = async () => { setLoading(true) try { + // Build URLs with search query if present + const hotUrl = debouncedSearchQuery + ? `/api/feed/hot?q=${encodeURIComponent(debouncedSearchQuery)}` + : '/api/feed/hot' + const newUrl = debouncedSearchQuery + ? `/api/feed/new?q=${encodeURIComponent(debouncedSearchQuery)}` + : '/api/feed/new' + // Fetch both hot and new rules in parallel const [hotResponse, newResponse] = await Promise.all([ - fetch('/api/feed/hot'), - fetch('/api/feed/new') + fetch(hotUrl), + fetch(newUrl) ]) if (hotResponse.ok) { @@ -160,7 +180,7 @@ export default function FeedPage() { } fetchRules() - }, []) + }, [debouncedSearchQuery]) const handleCopyContent = async (rule: CursorRule) => { await navigator.clipboard.writeText(rule.content) @@ -205,6 +225,26 @@ export default function FeedPage() {

Discover popular and recently created cursor rules from the community

+ {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="pl-10 pr-10 bg-[#1B1D21] border-white/10 text-white placeholder:text-gray-400 focus:border-[#70A7D7] focus:ring-[#70A7D7]" + /> + {searchQuery && ( + + )} +
+ {/* Tabs */}
) : ( currentRules.map((rule, index) => ( diff --git a/components/dashboard/user-lists.tsx b/components/dashboard/user-lists.tsx index e045783..dda7da5 100644 --- a/components/dashboard/user-lists.tsx +++ b/components/dashboard/user-lists.tsx @@ -94,6 +94,7 @@ export function UserLists({ onListsChange }: UserListsProps) { const [newListTitle, setNewListTitle] = useState("") const [isCreating, setIsCreating] = useState(false) const [actionStates, setActionStates] = useState<{[key: string]: boolean}>({}) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) // Helper function to show checkmark feedback const showActionFeedback = (actionKey: string) => { @@ -161,6 +162,7 @@ export function UserLists({ onListsChange }: UserListsProps) { const newList = await response.json() setLists(prev => [newList, ...prev]) setNewListTitle("") + setIsCreateDialogOpen(false) // Close the dialog after successful creation toast.success(`Created list "${newListTitle}"!`) track("List Created", { listId: newList.id }) onListsChange?.() @@ -293,7 +295,7 @@ export function UserLists({ onListsChange }: UserListsProps) {
{loading ? "Loading..." : `${lists.length} list${lists.length !== 1 ? 's' : ''}`}
- +