diff --git a/README.md b/README.md index 44b212d..de780a5 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ A modern, self-hostable GitLab frontend that works with **public repositories ou [![Docker](https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white)](Dockerfile) [![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=white)](https://react.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) -[![Live Demo](https://img.shields.io/badge/Live%20Demo-DigitalOcean-0080FF?logo=digitalocean&logoColor=white)](https://glabrowser-f3lru.ondigitalocean.app/) +[![Live Demo](https://img.shields.io/badge/Live%20Demo-DigitalOcean-0080FF?logo=digitalocean&logoColor=white)](https://glabrowser-bchpz.ondigitalocean.app/) -> **🌐 Live demo:** [https://glabrowser-f3lru.ondigitalocean.app/](https://glabrowser-f3lru.ondigitalocean.app/) — browse public GitLab repositories instantly. +> **🌐 Live demo:** [https://glabrowser-bchpz.ondigitalocean.app/](https://glabrowser-bchpz.ondigitalocean.app/) — browse public GitLab repositories instantly. > **Note**: This project is not affiliated with or endorsed by GitLab Inc. It uses the public [GitLab REST API](https://docs.gitlab.com/api/rest/). diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx index c1f8c6a..89dfa2f 100644 --- a/src/pages/Search.tsx +++ b/src/pages/Search.tsx @@ -1,14 +1,16 @@ import { useState, useEffect } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { Search as SearchIcon, FolderOpen, AlertCircle, GitPullRequest, GitCommit, FileCode } from 'lucide-react'; +import { Search as SearchIcon, FolderOpen, AlertCircle, GitPullRequest, GitCommit, FileCode, Lock } from 'lucide-react'; import { Input } from '../components/ui/input'; import { Button } from '../components/ui/button'; import { Skeleton } from '../components/ui/skeleton'; import { Badge } from '../components/ui/badge'; import EmptyState from '../components/common/EmptyState'; import { useApi } from '../api'; +import { useAuthStore } from '../store/auth'; import type { SearchScope } from '../api/search'; +import type { GitLabSearchResult } from '../types/gitlab'; const SCOPES: Array<{ value: SearchScope; label: string; icon: React.ElementType }> = [ { value: 'projects', label: 'Projects', icon: FolderOpen }, @@ -20,6 +22,8 @@ const SCOPES: Array<{ value: SearchScope; label: string; icon: React.ElementType export default function Search() { const api = useApi(); + const { token, user } = useAuthStore(); + const isGuest = token === '' && !user; const [searchParams, setSearchParams] = useSearchParams(); const [query, setQuery] = useState(searchParams.get('q') ?? ''); const [scope, setScope] = useState('projects'); @@ -30,10 +34,36 @@ export default function Search() { setQuery(q); }, [q]); - const { data, isLoading } = useQuery({ - queryKey: ['search', scope, q], - queryFn: () => api.search.global(scope, q, { per_page: 20 }), - enabled: q.length > 1, + // Scopes that require authentication — disabled in guest mode. + const AUTH_REQUIRED_SCOPES: SearchScope[] = ['issues', 'merge_requests', 'commits', 'blobs']; + + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['search', scope, q, isGuest], + queryFn: async () => { + // Guest mode: the global /search endpoint requires authentication. + // For the "projects" scope, fall back to /projects?search=...&visibility=public + // which works without a token. All other scopes are blocked in guest mode. + if (isGuest) { + const result = await api.projects.list({ + search: q, + visibility: 'public', + per_page: 20, + order_by: 'last_activity_at', + }); + return { + items: result.items.map((p): GitLabSearchResult => ({ + id: p.id, + title: p.name_with_namespace, + description: p.description ?? undefined, + web_url: p.web_url, + })), + pagination: result.pagination, + }; + } + return api.search.global(scope, q, { per_page: 20 }); + }, + // In guest mode only the projects scope is supported. + enabled: q.length > 1 && (!isGuest || scope === 'projects'), }); const handleSearch = (e: React.FormEvent) => { @@ -65,22 +95,37 @@ export default function Search() { <> {/* Scope tabs */}
- {SCOPES.map(({ value, label, icon: Icon }) => ( - - ))} + {SCOPES.map(({ value, label, icon: Icon }) => { + const locked = isGuest && AUTH_REQUIRED_SCOPES.includes(value); + return ( + + ); + })}
+ {/* Guest-mode notice for non-project scopes */} + {isGuest && AUTH_REQUIRED_SCOPES.includes(scope) && ( +
+ Searching {scope.replace('_', ' ')} requires a Personal Access Token.{' '} + Sign in to unlock full search. +
+ )} + {/* Results */}
{isLoading ? ( @@ -90,6 +135,16 @@ export default function Search() {
)) + ) : isError ? ( + } + title="Search failed" + description={ + (error as Error)?.message?.includes('401') || (error as Error)?.message?.includes('403') + ? 'This search scope requires authentication. Please sign in.' + : 'Could not reach the GitLab API. Check your connection and try again.' + } + /> ) : !data?.items.length ? ( } @@ -160,7 +215,11 @@ export default function Search() { } title="Search GitLab" - description="Search for projects, issues, merge requests, commits, and code across all your GitLab instances." + description={ + isGuest + ? 'Search for public projects. Sign in to also search issues, merge requests, commits, and code.' + : 'Search for projects, issues, merge requests, commits, and code across all your GitLab instances.' + } /> )}