Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
a517988482
commit
004b865e47
222
app/api/search/route.ts
Normal file
222
app/api/search/route.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
// Search result types - extendable for future search types
|
||||||
|
export type SearchResultType =
|
||||||
|
| "task"
|
||||||
|
| "project"
|
||||||
|
| "document"
|
||||||
|
| "sprint"
|
||||||
|
| "activity";
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
type: SearchResultType;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
description?: string;
|
||||||
|
status?: string;
|
||||||
|
priority?: string;
|
||||||
|
color?: string;
|
||||||
|
url?: string;
|
||||||
|
icon?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponse {
|
||||||
|
results: SearchResult[];
|
||||||
|
total: number;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search configuration - easily extensible
|
||||||
|
interface SearchConfig {
|
||||||
|
table: string;
|
||||||
|
type: SearchResultType;
|
||||||
|
fields: string[];
|
||||||
|
titleField: string;
|
||||||
|
subtitleField?: string;
|
||||||
|
descriptionField?: string;
|
||||||
|
statusField?: string;
|
||||||
|
priorityField?: string;
|
||||||
|
colorField?: string;
|
||||||
|
urlGenerator: (item: any) => string;
|
||||||
|
icon: string;
|
||||||
|
enabled: boolean;
|
||||||
|
searchFields: string[]; // Fields to search in
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define searchable entities - add new ones here
|
||||||
|
const searchConfigs: SearchConfig[] = [
|
||||||
|
{
|
||||||
|
table: "tasks",
|
||||||
|
type: "task",
|
||||||
|
fields: ["id", "title", "description", "status", "priority", "project_id", "type"],
|
||||||
|
titleField: "title",
|
||||||
|
subtitleField: "type",
|
||||||
|
descriptionField: "description",
|
||||||
|
statusField: "status",
|
||||||
|
priorityField: "priority",
|
||||||
|
urlGenerator: (item) => `https://gantt-board.vercel.app/tasks/${item.id}`,
|
||||||
|
icon: "kanban",
|
||||||
|
enabled: true,
|
||||||
|
searchFields: ["title", "description"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: "projects",
|
||||||
|
type: "project",
|
||||||
|
fields: ["id", "name", "description", "color", "status"],
|
||||||
|
titleField: "name",
|
||||||
|
descriptionField: "description",
|
||||||
|
colorField: "color",
|
||||||
|
statusField: "status",
|
||||||
|
urlGenerator: (item) => `/projects`,
|
||||||
|
icon: "folder-kanban",
|
||||||
|
enabled: true,
|
||||||
|
searchFields: ["name", "description"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: "sprints",
|
||||||
|
type: "sprint",
|
||||||
|
fields: ["id", "name", "goal", "status", "start_date", "end_date", "project_id"],
|
||||||
|
titleField: "name",
|
||||||
|
subtitleField: "goal",
|
||||||
|
statusField: "status",
|
||||||
|
urlGenerator: (item) => `https://gantt-board.vercel.app/sprints/${item.id}`,
|
||||||
|
icon: "timer",
|
||||||
|
enabled: true,
|
||||||
|
searchFields: ["name", "goal"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: "mission_control_documents",
|
||||||
|
type: "document",
|
||||||
|
fields: ["id", "title", "content", "folder", "tags"],
|
||||||
|
titleField: "title",
|
||||||
|
subtitleField: "folder",
|
||||||
|
descriptionField: "content",
|
||||||
|
urlGenerator: (item) => `/documents`,
|
||||||
|
icon: "file-text",
|
||||||
|
enabled: true,
|
||||||
|
searchFields: ["title", "content"],
|
||||||
|
},
|
||||||
|
// Future: Add more search types here
|
||||||
|
// {
|
||||||
|
// table: "meetings",
|
||||||
|
// type: "meeting",
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const query = searchParams.get("q")?.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
return NextResponse.json({
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
query: query || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Supabase client with service role for full access
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Database not configured" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
|
const results: SearchResult[] = [];
|
||||||
|
|
||||||
|
// Search each configured entity
|
||||||
|
for (const config of searchConfigs) {
|
||||||
|
if (!config.enabled) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build OR filter dynamically based on searchFields
|
||||||
|
const orConditions = config.searchFields
|
||||||
|
.map(field => `${field}.ilike.%${query}%`)
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from(config.table)
|
||||||
|
.select(config.fields.join(", "))
|
||||||
|
.or(orConditions)
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(`Search error in ${config.table}:`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const mappedResults: SearchResult[] = data.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
type: config.type,
|
||||||
|
title: item[config.titleField] || "Untitled",
|
||||||
|
subtitle: config.subtitleField ? item[config.subtitleField] : undefined,
|
||||||
|
description: config.descriptionField ? item[config.descriptionField] : undefined,
|
||||||
|
status: config.statusField ? item[config.statusField] : undefined,
|
||||||
|
priority: config.priorityField ? item[config.priorityField] : undefined,
|
||||||
|
color: config.colorField ? item[config.colorField] : undefined,
|
||||||
|
url: config.urlGenerator(item),
|
||||||
|
icon: config.icon,
|
||||||
|
metadata: {
|
||||||
|
table: config.table,
|
||||||
|
...Object.entries(item).reduce((acc, [key, value]) => {
|
||||||
|
if (typeof value === "string" || typeof value === "number") {
|
||||||
|
acc[key] = String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
results.push(...mappedResults);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error searching ${config.table}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort results by relevance (exact matches first, then partial)
|
||||||
|
results.sort((a, b) => {
|
||||||
|
const aTitle = a.title.toLowerCase();
|
||||||
|
const bTitle = b.title.toLowerCase();
|
||||||
|
|
||||||
|
// Exact match gets highest priority
|
||||||
|
if (aTitle === query && bTitle !== query) return -1;
|
||||||
|
if (bTitle === query && aTitle !== query) return 1;
|
||||||
|
|
||||||
|
// Starts with query gets second priority
|
||||||
|
if (aTitle.startsWith(query) && !bTitle.startsWith(query)) return -1;
|
||||||
|
if (bTitle.startsWith(query) && !aTitle.startsWith(query)) return 1;
|
||||||
|
|
||||||
|
// Otherwise alphabetical
|
||||||
|
return aTitle.localeCompare(bTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Limit total results
|
||||||
|
const limitedResults = results.slice(0, 50);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
results: limitedResults,
|
||||||
|
total: limitedResults.length,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Search failed" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,20 +25,25 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Timer,
|
||||||
|
Circle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { supabaseClient } from "@/lib/supabase/client";
|
|
||||||
|
|
||||||
interface Task {
|
// Search result type from API
|
||||||
|
type SearchResultType = "task" | "project" | "document" | "sprint" | "activity";
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
|
type: SearchResultType;
|
||||||
title: string;
|
title: string;
|
||||||
status: string;
|
subtitle?: string;
|
||||||
priority: string;
|
description?: string;
|
||||||
project_id: string;
|
metadata?: Record<string, string>;
|
||||||
}
|
status?: string;
|
||||||
|
priority?: string;
|
||||||
interface Project {
|
color?: string;
|
||||||
id: string;
|
url?: string;
|
||||||
name: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@ -58,18 +63,49 @@ const quickLinks = [
|
|||||||
{ name: "Gitea", url: "http://192.168.1.128:3000", icon: ExternalLink },
|
{ name: "Gitea", url: "http://192.168.1.128:3000", icon: ExternalLink },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getStatusIcon(status: string) {
|
// Icon mapping for search result types
|
||||||
|
const typeIcons: Record<SearchResultType, React.ElementType> = {
|
||||||
|
task: Kanban,
|
||||||
|
project: FolderKanban,
|
||||||
|
document: FileText,
|
||||||
|
sprint: Timer,
|
||||||
|
activity: Activity,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tag colors for documents
|
||||||
|
const tagColors: Record<string, string> = {
|
||||||
|
infrastructure: "bg-blue-500/20 text-blue-400",
|
||||||
|
monitoring: "bg-green-500/20 text-green-400",
|
||||||
|
security: "bg-red-500/20 text-red-400",
|
||||||
|
urgent: "bg-orange-500/20 text-orange-400",
|
||||||
|
guide: "bg-purple-500/20 text-purple-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type labels
|
||||||
|
const typeLabels: Record<SearchResultType, string> = {
|
||||||
|
task: "Task",
|
||||||
|
project: "Project",
|
||||||
|
document: "Document",
|
||||||
|
sprint: "Sprint",
|
||||||
|
activity: "Activity",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStatusIcon(status?: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "done":
|
case "done":
|
||||||
|
case "completed":
|
||||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
||||||
case "in-progress":
|
case "in-progress":
|
||||||
return <Clock className="w-4 h-4 text-blue-500" />;
|
return <Clock className="w-4 h-4 text-blue-500" />;
|
||||||
|
case "open":
|
||||||
|
case "todo":
|
||||||
|
return <Circle className="w-4 h-4 text-muted-foreground" />;
|
||||||
default:
|
default:
|
||||||
return <AlertCircle className="w-4 h-4 text-muted-foreground" />;
|
return <AlertCircle className="w-4 h-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPriorityColor(priority: string): string {
|
function getPriorityColor(priority?: string): string {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case "urgent":
|
case "urgent":
|
||||||
return "text-red-500";
|
return "text-red-500";
|
||||||
@ -84,42 +120,36 @@ function getPriorityColor(priority: string): string {
|
|||||||
|
|
||||||
export function QuickSearch() {
|
export function QuickSearch() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [query, setQuery] = useState("");
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Fetch tasks when search opens
|
// Debounced search
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open || query.length < 2) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const fetchData = async () => {
|
const timer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch tasks
|
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
|
||||||
const { data: tasksData } = await supabaseClient
|
const data = await res.json();
|
||||||
.from('tasks')
|
setResults(data.results || []);
|
||||||
.select('id, title, status, priority, project_id')
|
|
||||||
.order('updated_at', { ascending: false })
|
|
||||||
.limit(50);
|
|
||||||
|
|
||||||
// Fetch projects for names
|
|
||||||
const { data: projectsData } = await supabaseClient
|
|
||||||
.from('projects')
|
|
||||||
.select('id, name');
|
|
||||||
|
|
||||||
setTasks(tasksData || []);
|
|
||||||
setProjects(projectsData || []);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching search data:', err);
|
console.error("Search error:", err);
|
||||||
|
setResults([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, 150); // 150ms debounce
|
||||||
|
|
||||||
fetchData();
|
return () => clearTimeout(timer);
|
||||||
}, [open]);
|
}, [query, open]);
|
||||||
|
|
||||||
|
// Keyboard shortcut
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
@ -133,13 +163,32 @@ export function QuickSearch() {
|
|||||||
|
|
||||||
const runCommand = useCallback((command: () => void) => {
|
const runCommand = useCallback((command: () => void) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
setQuery("");
|
||||||
command();
|
command();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getProjectName = (projectId: string) => {
|
const handleResultSelect = (result: SearchResult) => {
|
||||||
return projects.find(p => p.id === projectId)?.name || 'Unknown';
|
runCommand(() => {
|
||||||
|
if (result.url?.startsWith("http")) {
|
||||||
|
window.open(result.url, "_blank");
|
||||||
|
} else if (result.url) {
|
||||||
|
router.push(result.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Group results by type
|
||||||
|
const groupedResults = results.reduce((acc, result) => {
|
||||||
|
if (!acc[result.type]) acc[result.type] = [];
|
||||||
|
acc[result.type].push(result);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<SearchResultType, SearchResult[]>);
|
||||||
|
|
||||||
|
// Get ordered list of types that have results
|
||||||
|
const activeTypes = (Object.keys(groupedResults) as SearchResultType[]).filter(
|
||||||
|
(type) => groupedResults[type]?.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Search Button Trigger */}
|
{/* Search Button Trigger */}
|
||||||
@ -157,9 +206,83 @@ export function QuickSearch() {
|
|||||||
|
|
||||||
{/* Command Dialog */}
|
{/* Command Dialog */}
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder="Type a command or search..." />
|
<CommandInput
|
||||||
|
placeholder="Search tasks, projects, sprints, documents..."
|
||||||
|
value={query}
|
||||||
|
onValueChange={setQuery}
|
||||||
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>
|
||||||
|
{loading ? "Searching..." : query.length < 2 ? "Type to search..." : "No results found."}
|
||||||
|
</CommandEmpty>
|
||||||
|
|
||||||
|
{/* Show search results when query exists */}
|
||||||
|
{query.length >= 2 && activeTypes.length > 0 && (
|
||||||
|
<>
|
||||||
|
{activeTypes.map((type) => {
|
||||||
|
const Icon = typeIcons[type];
|
||||||
|
const typeResults = groupedResults[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandGroup key={type} heading={`${typeLabels[type]}s (${typeResults.length})`}>
|
||||||
|
{typeResults.slice(0, 5).map((result) => (
|
||||||
|
<CommandItem
|
||||||
|
key={`${result.type}-${result.id}`}
|
||||||
|
onSelect={() => handleResultSelect(result)}
|
||||||
|
>
|
||||||
|
{result.type === "task" ? (
|
||||||
|
getStatusIcon(result.status)
|
||||||
|
) : result.type === "project" && result.color ? (
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full mr-2"
|
||||||
|
style={{ backgroundColor: result.color }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<span className="truncate">{result.title}</span>
|
||||||
|
{result.subtitle && (
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{result.subtitle}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document tags */}
|
||||||
|
{result.type === "document" && result.metadata?.tags && (
|
||||||
|
<div className="flex gap-1 ml-2">
|
||||||
|
{result.metadata.tags.split(",").slice(0, 2).map((tag: string) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className={`text-[10px] px-1.5 py-0.5 rounded ${tagColors[tag.trim()] || "bg-muted text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
{tag.trim()}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.priority && (
|
||||||
|
<span className={`text-xs ${getPriorityColor(result.priority)} ml-2`}>
|
||||||
|
{result.priority}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.status && result.type !== "task" && (
|
||||||
|
<span className="text-xs text-muted-foreground ml-2 capitalize">
|
||||||
|
{result.status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<CommandSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<CommandGroup heading="Navigation">
|
<CommandGroup heading="Navigation">
|
||||||
@ -194,37 +317,6 @@ export function QuickSearch() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
||||||
{/* Tasks */}
|
|
||||||
{tasks.length > 0 && (
|
|
||||||
<>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup heading={`Tasks (${tasks.length})`}>
|
|
||||||
{tasks.slice(0, 10).map((task) => (
|
|
||||||
<CommandItem
|
|
||||||
key={task.id}
|
|
||||||
onSelect={() =>
|
|
||||||
runCommand(() =>
|
|
||||||
window.open(
|
|
||||||
`https://gantt-board.vercel.app/tasks/${task.id}`,
|
|
||||||
"_blank"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{getStatusIcon(task.status)}
|
|
||||||
<span className="mr-2 truncate">{task.title}</span>
|
|
||||||
<span className={`text-xs ${getPriorityColor(task.priority)}`}>
|
|
||||||
{task.priority}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground ml-auto">
|
|
||||||
{getProjectName(task.project_id)}
|
|
||||||
</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
||||||
|
|||||||
27
search.ts
Normal file
27
search.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
|
|
||||||
|
async function searchMissionControl() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('mission_control_documents')
|
||||||
|
.select('title, content')
|
||||||
|
.or('title.ilike.%mission%,content.ilike.%mission%');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Query error:', error);
|
||||||
|
} else {
|
||||||
|
console.log('Search results:', data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unexpected error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
google('Mission Control Strategy Plan')
|
||||||
|
|
||||||
|
searchMissionControl();
|
||||||
25
test-search.js
Normal file
25
test-search.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
|
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
|
|
||||||
|
async function testSearch() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('mission_control_documents')
|
||||||
|
.select('title, content')
|
||||||
|
.or('title.ilike.%mission%,content.ilike.%mission%');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Query error:', error);
|
||||||
|
} else {
|
||||||
|
console.log('Search results:', data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unexpected error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testSearch();
|
||||||
Loading…
Reference in New Issue
Block a user