diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx index 166431b..86c31de 100644 --- a/src/components/AuthButton.tsx +++ b/src/components/AuthButton.tsx @@ -1,15 +1,19 @@ "use client"; import { useSession, signIn, signOut } from "next-auth/react"; -import { Github, LogOut, LayoutDashboard, User } from "lucide-react"; +import { Github, LogOut, LayoutDashboard, User, Key, CheckCircle } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; +import { GitHubTokenModal } from "./GitHubTokenModal"; +import { useGitHubToken } from "@/lib/use-github-token"; export default function AuthButton() { const { data: session, status } = useSession(); const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isTokenModalOpen, setIsTokenModalOpen] = useState(false); + const { token, setToken, clearToken } = useGitHubToken(); if (status === "loading") { return ( @@ -17,81 +21,113 @@ export default function AuthButton() { ); } - if (session) { - return ( -
+ return ( + <> +
- - - {isMenuOpen && ( + {token ? ( + <> + + Your token + + ) : ( <> - setIsMenuOpen(false)} - className="fixed inset-0 z-40" - /> - - setIsMenuOpen(false)} - > - - Dashboard - -
- - + + Add token )} - + + + {session ? ( +
+ + + + {isMenuOpen && ( + <> + setIsMenuOpen(false)} + className="fixed inset-0 z-40" + /> + + setIsMenuOpen(false)} + > + + Dashboard + +
+ + + + )} + +
+ ) : ( + + )}
- ); - } - return ( - + setIsTokenModalOpen(false)} + currentToken={token} + onSave={setToken} + onClear={clearToken} + /> + ); } diff --git a/src/components/GitHubTokenModal.tsx b/src/components/GitHubTokenModal.tsx new file mode 100644 index 0000000..9245850 --- /dev/null +++ b/src/components/GitHubTokenModal.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { X, Key, Eye, EyeOff, CheckCircle, AlertCircle, Trash2, ExternalLink } from "lucide-react"; + +interface GitHubTokenModalProps { + isOpen: boolean; + onClose: () => void; + currentToken: string | null; + onSave: (token: string) => void; + onClear: () => void; +} + +type ValidationState = "idle" | "validating" | "valid" | "invalid"; + +export function GitHubTokenModal({ + isOpen, + onClose, + currentToken, + onSave, + onClear, +}: GitHubTokenModalProps) { + const [value, setValue] = useState(""); + const [showToken, setShowToken] = useState(false); + const [validation, setValidation] = useState("idle"); + const [validatedUser, setValidatedUser] = useState(null); + const inputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setValue(""); + setValidation("idle"); + setValidatedUser(null); + setTimeout(() => inputRef.current?.focus(), 50); + } + }, [isOpen]); + + if (!isOpen) return null; + + const handleValidate = async () => { + const trimmed = value.trim(); + if (!trimmed) return; + setValidation("validating"); + setValidatedUser(null); + try { + const res = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${trimmed}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + if (res.ok) { + const data = await res.json(); + setValidatedUser(data.login ?? "unknown"); + setValidation("valid"); + } else { + setValidation("invalid"); + } + } catch { + setValidation("invalid"); + } + }; + + const handleSave = () => { + const trimmed = value.trim(); + if (!trimmed || validation !== "valid") return; + onSave(trimmed); + setValue(""); + onClose(); + }; + + const handleClear = () => { + onClear(); + setValue(""); + setValidation("idle"); + setValidatedUser(null); + onClose(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") onClose(); + if (e.key === "Enter" && validation === "idle") handleValidate(); + if (e.key === "Enter" && validation === "valid") handleSave(); + }; + + return ( +
e.target === e.currentTarget && onClose()} + > +
+ +
+
+ +
+

Use your GitHub token

+

+ Paste a Personal Access Token {" "} + to remove rate limits and access private repositories. Your token is stored only in this browser — never logged or sent to our servers. +

+ {currentToken && ( +
+ + + Token active + + +
+ )} +
+ { + setValue(e.target.value); + setValidation("idle"); + setValidatedUser(null); + }} + onKeyDown={handleKeyDown} + placeholder={currentToken ? "Paste new token to replace…" : "ghp_xxxxxxxxxxxxxxxxxxxx"} + className="w-full px-4 py-3 pr-12 bg-zinc-800 border border-white/10 rounded-xl text-white text-sm placeholder:text-zinc-600 focus:outline-none focus:ring-2 focus:ring-emerald-500/40 focus:border-emerald-500/40 transition-all font-mono" + autoComplete="off" + spellCheck={false} + /> + +
+ {validation === "validating" && ( +

+ + Verifying token… +

+ )} + {validation === "valid" && validatedUser && ( +

+ + Authenticated as {validatedUser} +

+ )} + {validation === "invalid" && ( +

+ + Token invalid or missing required scopes +

+ )} +
+ {validation !== "valid" ? ( + + ) : ( + + )} +
+

+ Minimum required scope:{" "} + public_repo + {" "}for public repos.{" "} + repo + {" "}for private access. +

+
+
+
+ ); +} diff --git a/src/lib/use-github-token.ts b/src/lib/use-github-token.ts new file mode 100644 index 0000000..37e7dff --- /dev/null +++ b/src/lib/use-github-token.ts @@ -0,0 +1,51 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +const STORAGE_KEY = "repomind_github_pat"; + +export type TokenMode = "server" | "personal"; + +export interface GitHubTokenState { + token: string | null; + mode: TokenMode; + setToken: (token: string) => void; + clearToken: () => void; +} + +export function useGitHubToken(): GitHubTokenState { + const [token, setTokenState] = useState(null); + + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) setTokenState(stored); + } catch {} + }, []); + + const setToken = useCallback((newToken: string) => { + const trimmed = newToken.trim(); + try { + if (trimmed) { + localStorage.setItem(STORAGE_KEY, trimmed); + } else { + localStorage.removeItem(STORAGE_KEY); + } + } catch {} + setTokenState(trimmed || null); + }, []); + + const clearToken = useCallback(() => { + try { + localStorage.removeItem(STORAGE_KEY); + } catch {} + setTokenState(null); + }, []); + + return { + token, + mode: token ? "personal" : "server", + setToken, + clearToken, + }; +}