diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..c6c7c105 --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +# TODO + +## Command palette search cleanup +- [ ] Edit `frontend/src/hooks/useCommandSearch.ts` + - [x] Remove redundant `useEffect` that computes `pages` but discards it + - [x] Ensure `projectsResults`, `developersResults`, `isLoading`, and `error` are deterministic when query length < 2 + +- [x] Run frontend lint +- [ ] Verify build/typecheck (if available) + + diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 36eb1098..f59511a6 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1,4 +1,5 @@ + > [!IMPORTANT] > This project is connected to [Lovable](https://lovable.dev). Avoid rewriting > published git history — force pushing, or rebasing/amending/squashing commits @@ -7,4 +8,5 @@ > > Commits you push to the connected branch sync back to Lovable and show up in > the editor, so keep the branch in a working state. + diff --git a/frontend/src/components/command-palette/CommandPalette.tsx b/frontend/src/components/command-palette/CommandPalette.tsx new file mode 100644 index 00000000..1b4fc9fb --- /dev/null +++ b/frontend/src/components/command-palette/CommandPalette.tsx @@ -0,0 +1,262 @@ +"use client"; + +import * as React from "react"; + +type CommandPaletteGroup = "pages" | "projects" | "developers"; + +export type CommandPaletteItemBase = { + id: string; + title: string; + description?: string; + group: CommandPaletteGroup; +}; + +type FlattenedResult = + | { type: "header"; id: string; label: string; group: CommandPaletteGroup } + | { type: "item"; item: TItem }; + +export type CommandPaletteProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + results: Array<{ + group: CommandPaletteGroup; + items: TItem[]; + }>; + onSelect: (item: TItem) => void; +}; + +function getGroupLabel(group: CommandPaletteGroup): string { + switch (group) { + case "pages": + return "Pages"; + case "projects": + return "Projects"; + case "developers": + return "Developers"; + } +} + +export function CommandPalette( + props: CommandPaletteProps, +): React.ReactElement { + const { open, onOpenChange, results, onSelect } = props; + + const searchInputRef = React.useRef(null); + const [highlightedIndex, setHighlightedIndex] = React.useState(-1); + + const flattened = React.useMemo[]>(() => { + const acc: FlattenedResult[] = []; + + for (const group of results) { + acc.push({ + type: "header", + id: `header-${group.group}`, + label: getGroupLabel(group.group), + group: group.group, + }); + + for (const item of group.items) { + acc.push({ type: "item", item }); + } + } + + return acc; + }, [results]); + + const itemIndices = React.useMemo(() => { + const indices: number[] = []; + for (let i = 0; i < flattened.length; i += 1) { + if (flattened[i].type === "item") indices.push(i); + } + return indices; + }, [flattened]); + + const highlightedItemIndexInItems = React.useMemo(() => { + if (highlightedIndex < 0) return -1; + const pos = itemIndices.indexOf(highlightedIndex); + return pos; + }, [highlightedIndex, itemIndices]); + + const focusSearch = React.useCallback(() => { + searchInputRef.current?.focus(); + }, []); + + React.useEffect(() => { + if (!open) return; + focusSearch(); + + const firstItemIndex = itemIndices[0] ?? -1; + setHighlightedIndex(firstItemIndex); + }, [open, focusSearch, itemIndices]); + + const close = React.useCallback(() => { + onOpenChange(false); + }, [onOpenChange]); + + const onBackdropMouseDown = (event: React.MouseEvent) => { + if (event.target === event.currentTarget) close(); + }; + + const selectedItem = React.useMemo(() => { + if (highlightedIndex < 0) return null; + const entry = flattened[highlightedIndex]; + if (!entry || entry.type !== "item") return null; + return entry.item; + }, [flattened, highlightedIndex]); + + const moveHighlight = React.useCallback( + (direction: 1 | -1) => { + if (itemIndices.length === 0) return; + + const currentPos = highlightedItemIndexInItems; + const nextPos = + currentPos === -1 + ? direction === 1 + ? 0 + : itemIndices.length - 1 + : (currentPos + direction + itemIndices.length) % itemIndices.length; + + setHighlightedIndex(itemIndices[nextPos] ?? -1); + }, + [itemIndices, highlightedItemIndexInItems], + ); + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + close(); + return; + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + moveHighlight(1); + return; + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + moveHighlight(-1); + return; + } + + if (event.key === "Tab") { + event.preventDefault(); + moveHighlight(event.shiftKey ? -1 : 1); + return; + } + + if (event.key === "Enter") { + if (!selectedItem) return; + event.preventDefault(); + onSelect(selectedItem); + close(); + } + }; + + if (!open) return ; + + return ( +
+
+ +
+
+
+ +
+ +
+
+
    + {flattened.map((entry, index) => { + if (entry.type === "header") { + return ( + + ); + } + + const item = entry.item; + const isSelected = index === highlightedIndex; + + return ( +
  • + +
  • + ); + })} +
+
+
+ +
+
+ Use ↑ ↓, Enter to select + Esc to close +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index f2f96260..f827174b 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -64,9 +64,7 @@ const groups: Group[] = [ }, { label: "Account", - items: [ - { label: "Settings", to: "/settings", icon: }, - ], + items: [{ label: "Settings", to: "/settings", icon: }], }, ]; @@ -147,10 +145,7 @@ function SidebarGroup({ group, onNav }: { group: Group; onNav: () => void }) { className="flex w-full items-center justify-between px-2 py-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground" > {group.label} - + {open && ( @@ -161,7 +156,8 @@ function SidebarGroup({ group, onNav }: { group: Group; onNav: () => void }) { className="overflow-hidden" > {group.items.map((item) => { - const active = pathname === item.to || pathname.startsWith(item.to.split("?")[0] + "/"); + const active = + pathname === item.to || pathname.startsWith(item.to.split("?")[0] + "/"); return (
  • {title} {action && (actionTo ? ( - + {action} ) : ( - + ))}
  • ); diff --git a/frontend/src/features/dashboard/GreetingHero.tsx b/frontend/src/features/dashboard/GreetingHero.tsx index 63eb5730..6ccd6494 100644 --- a/frontend/src/features/dashboard/GreetingHero.tsx +++ b/frontend/src/features/dashboard/GreetingHero.tsx @@ -73,7 +73,9 @@ function MiniStat({

    {label}

    {value} - {suffix && {suffix}} + {suffix && ( + {suffix} + )}

    {progress !== undefined && (
    diff --git a/frontend/src/features/dashboard/sections.tsx b/frontend/src/features/dashboard/sections.tsx index 3d82a180..b6a20699 100644 --- a/frontend/src/features/dashboard/sections.tsx +++ b/frontend/src/features/dashboard/sections.tsx @@ -124,7 +124,12 @@ export function InviteRequests() {
      {data.map((r) => (
    • - + {r.icon}
      @@ -170,9 +175,7 @@ export function SuggestedBuilders() { {s} ))}
      -

      - {b.yearsExp} yrs exp -

      +

      {b.yearsExp} yrs exp

      {b.matchScore}% Match

    • @@ -354,14 +393,22 @@ export function UpcomingDeadlines() { } export function NotificationsFeed() { - const { data = [] } = useQuery({ queryKey: ["notifications"], queryFn: notificationsService.list }); + const { data = [] } = useQuery({ + queryKey: ["notifications"], + queryFn: notificationsService.list, + }); return (
        {data.map((n) => (
      • - +

        {n.text}

        {n.ago}
      • diff --git a/frontend/src/hooks/useCommandPalette.ts b/frontend/src/hooks/useCommandPalette.ts new file mode 100644 index 00000000..56fb9d47 --- /dev/null +++ b/frontend/src/hooks/useCommandPalette.ts @@ -0,0 +1,44 @@ +"use client"; + +import * as React from "react"; + +type UseCommandPaletteResult = { + open: boolean; + setOpen: React.Dispatch>; + toggle: () => void; +}; + +function isMacLikePlatform(): boolean { + const platform = navigator.platform?.toLowerCase?.() ?? ""; + const userAgent = navigator.userAgent?.toLowerCase?.() ?? ""; + return platform.includes("mac") || userAgent.includes("mac os"); +} + +export function useCommandPalette(): UseCommandPaletteResult { + const [open, setOpen] = React.useState(false); + + const toggle = React.useCallback(() => { + setOpen((prev) => !prev); + }, []); + + React.useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + const isMac = isMacLikePlatform(); + const kPressed = event.key.toLowerCase() === "k"; + + const shouldTrigger = isMac ? event.metaKey && kPressed : event.ctrlKey && kPressed; + if (!shouldTrigger) return; + + event.preventDefault(); + event.stopPropagation(); + toggle(); + }; + + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [toggle]); + + return { open, setOpen, toggle }; +} diff --git a/frontend/src/hooks/useCommandSearch.ts b/frontend/src/hooks/useCommandSearch.ts new file mode 100644 index 00000000..3cd69ad9 --- /dev/null +++ b/frontend/src/hooks/useCommandSearch.ts @@ -0,0 +1,148 @@ +"use client"; + +import * as React from "react"; + +import { + searchProjects, + searchUsers, + type ProjectSearchResult, + type DeveloperSearchResult, +} from "@/lib/api"; + +import { includesCaseInsensitive } from "@/lib/commandSearchUtils"; + +export type CommandPalettePageId = + "dashboard" | "projects" | "builders" | "messages" | "notifications" | "settings"; + +export type CommandPaletteRouteResult = { + id: string; + title: string; + description?: string; + group: "pages" | "projects" | "developers"; +}; + +const STATIC_PAGES: Array<{ id: CommandPalettePageId; title: string; matchTokens: string[] }> = [ + { id: "dashboard", title: "Dashboard", matchTokens: ["dashboard", "home"] }, + { id: "projects", title: "Projects", matchTokens: ["projects", "project"] }, + { + id: "builders", + title: "Builder's Flare", + matchTokens: ["flare", "builder's flare", "builders"], + }, + { id: "messages", title: "Messages", matchTokens: ["messages", "inbox", "chat"] }, + { + id: "notifications", + title: "Notifications", + matchTokens: ["notifications", "alerts", "updates"], + }, + { id: "settings", title: "Settings", matchTokens: ["settings", "preferences", "profile"] }, +]; + +function normalizeQuery(query: string): string { + return query.trim(); +} + +function toDeveloperResult(user: DeveloperSearchResult): CommandPaletteRouteResult { + return { + id: user.id, + title: user.username, + description: user.headline, + group: "developers", + }; +} + +function toProjectResult(project: ProjectSearchResult): CommandPaletteRouteResult { + return { + id: project.id, + title: project.title, + description: project.slug, + group: "projects", + }; +} + +export type UseCommandSearchResult = { + pages: CommandPaletteRouteResult[]; + projects: CommandPaletteRouteResult[]; + developers: CommandPaletteRouteResult[]; + isLoading: boolean; + error: Error | null; +}; + +export function useCommandSearch(query: string): UseCommandSearchResult { + const [debouncedQuery, setDebouncedQuery] = React.useState(() => normalizeQuery(query)); + const [isLoading, setIsLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const [projectsResults, setProjectsResults] = React.useState([]); + const [developersResults, setDevelopersResults] = React.useState([]); + + React.useEffect(() => { + const next = normalizeQuery(query); + const handle = window.setTimeout(() => setDebouncedQuery(next), 250); + return () => window.clearTimeout(handle); + }, [query]); + + const pages = React.useMemo(() => { + const q = debouncedQuery; + if (!q) { + return STATIC_PAGES.map((p) => ({ id: p.id, title: p.title, group: "pages" })); + } + + return STATIC_PAGES.filter((p) => p.matchTokens.some((t) => includesCaseInsensitive(t, q))).map( + (p) => ({ + id: p.id, + title: p.title, + group: "pages", + }), + ); + }, [debouncedQuery]); + + React.useEffect(() => { + const q = debouncedQuery; + + // Keep these values deterministic for short queries. + if (q.length < 2) { + setProjectsResults([]); + setDevelopersResults([]); + setIsLoading(false); + setError(null); + return; + } + + const controller = new AbortController(); + + setIsLoading(true); + setError(null); + + Promise.all([searchProjects(q, controller.signal), searchUsers(q, controller.signal)]) + .then(([projects, users]) => { + if (controller.signal.aborted) return; + + setProjectsResults(projects.map(toProjectResult)); + setDevelopersResults(users.map(toDeveloperResult)); + }) + .catch((err: unknown) => { + if (controller.signal.aborted) return; + + const nextErr = + err instanceof Error ? err : new Error("Unknown error while searching command palette."); + setError(nextErr); + setProjectsResults([]); + setDevelopersResults([]); + }) + .finally(() => { + if (controller.signal.aborted) return; + setIsLoading(false); + }); + + return () => controller.abort(); + }, [debouncedQuery]); + + return { + pages, + projects: projectsResults, + developers: developersResults, + isLoading, + error, + }; +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4dd18b3d..92e2f25f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,170 +1,37 @@ -import { toast } from "sonner"; +"use client"; -type JsonPrimitive = string | number | boolean | null; -type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; - -export type ApplicationStatus = - | "pending" - | "reviewing" - | "accepted" - | "rejected" - | "withdrawn"; - -export type UUID = string; - -export type ApplicationResponse = { - id: UUID; - applicant_id: UUID; - project_id: UUID; - flare_id: UUID; - status: ApplicationStatus; - message: string | null; - portfolio_url: string | null; - github_url: string | null; - resume_url: string | null; - review_notes: string | null; - shortlisted: boolean; - created_at: string; - updated_at: string; -}; - -export type ApplicationCreatePayload = { - project_id: UUID; - flare_id: UUID; - message?: string; - portfolio_url?: string; - github_url?: string; - resume_url?: string; -}; - -export type ApplicationUpdatePayload = { - message?: string | null; - portfolio_url?: string | null; - github_url?: string | null; - resume_url?: string | null; - review_notes?: string | null; - shortlisted?: boolean; - status?: ApplicationStatus; +export type ProjectSearchResult = { + id: string; + title: string; + slug: string; }; -type ApiConfig = { - baseUrl: string; +export type DeveloperSearchResult = { + id: string; + username: string; + headline: string; }; -function getApiConfig(): ApiConfig { - const baseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined; - - return { - baseUrl: baseUrl && baseUrl.trim().length > 0 ? baseUrl : "", - }; -} - -function assertJson(data: unknown): T { - return data as T; -} - -async function requestJson( - input: { - url: string; - method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; - body?: TBody; - }, -): Promise { - const { baseUrl } = getApiConfig(); - - const res = await fetch(`${baseUrl}${input.url}`, { - method: input.method, - headers: input.body === undefined ? undefined : { "content-type": "application/json" }, - body: input.body === undefined ? undefined : JSON.stringify(input.body), - credentials: "include", - }); - - if (!res.ok) { - let message = `Request failed (${res.status})`; - - try { - const data: unknown = await res.json(); - if (typeof data === "object" && data !== null) { - const maybe = data as Record; - const m = maybe["detail"] ?? maybe["message"]; - if (typeof m === "string") message = m; - } - } catch { - // ignore json parse errors - } - - throw new Error(message); +async function fetchJson(signal: AbortSignal, url: string): Promise { + const response = await fetch(url, { method: "GET", signal }); + if (!response.ok) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); } - - if (res.status === 204) { - return undefined as unknown as TResponse; - } - - const data: unknown = await res.json(); - return assertJson(data); -} - -export async function applyToFlare( - flareId: UUID, - projectId: UUID, - payload: { - message?: string; - portfolio_url?: string; - github_url?: string; - }, -): Promise { - const body: ApplicationCreatePayload = { - project_id: projectId, - flare_id: flareId, - message: payload.message, - portfolio_url: payload.portfolio_url, - github_url: payload.github_url, - }; - - return requestJson({ - url: "/applications/", - method: "POST", - body, - }); -} - -export async function getMyApplications(): Promise { - return requestJson({ - url: "/applications/me", - method: "GET", - }); + return (await response.json()) as T; } -export async function getProjectApplications(projectId: UUID): Promise { - return requestJson({ - url: `/applications/project/${projectId}`, - method: "GET", - }); +export async function searchProjects( + query: string, + signal: AbortSignal, +): Promise { + const params = new URLSearchParams({ query }); + return fetchJson(signal, `/api/projects?${params.toString()}`); } -export async function acceptApplication(id: UUID): Promise { - return requestJson({ - url: `/applications/${id}/accept`, - method: "PATCH", - }); +export async function searchUsers( + query: string, + signal: AbortSignal, +): Promise { + const params = new URLSearchParams({ query }); + return fetchJson(signal, `/api/users?${params.toString()}`); } - -export async function rejectApplication(id: UUID): Promise { - return requestJson({ - url: `/applications/${id}/reject`, - method: "PATCH", - }); -} - -export async function withdrawApplication(id: UUID): Promise { - return requestJson({ - url: `/applications/${id}/withdraw`, - method: "PATCH", - }); -} - -export function toastError(err: unknown, fallback = "Something went wrong") { - const message = err instanceof Error ? err.message : fallback; - toast.error(message); -} - diff --git a/frontend/src/lib/commandSearchUtils.ts b/frontend/src/lib/commandSearchUtils.ts new file mode 100644 index 00000000..b9f261fc --- /dev/null +++ b/frontend/src/lib/commandSearchUtils.ts @@ -0,0 +1,7 @@ +"use client"; + +export function includesCaseInsensitive(haystack: string, needle: string): boolean { + const h = haystack.toLowerCase(); + const n = needle.toLowerCase(); + return h.includes(n); +} diff --git a/frontend/src/lib/logo.ts b/frontend/src/lib/logo.ts index b6de47d3..201bdac5 100644 --- a/frontend/src/lib/logo.ts +++ b/frontend/src/lib/logo.ts @@ -1,3 +1,3 @@ import logo from "@/assets/lodo-dev.jpeg"; -export const APP_LOGO = logo; \ No newline at end of file +export const APP_LOGO = logo; diff --git a/frontend/src/mocks/seed.ts b/frontend/src/mocks/seed.ts index 26882a3d..75c84b18 100644 --- a/frontend/src/mocks/seed.ts +++ b/frontend/src/mocks/seed.ts @@ -3,7 +3,9 @@ export type ID = string; -export interface Skill { name: string; } +export interface Skill { + name: string; +} export interface Builder { id: ID; name: string; @@ -110,27 +112,253 @@ const AV = (seed: string) => `https://api.dicebear.com/9.x/notionists-neutral/svg?seed=${encodeURIComponent(seed)}&backgroundColor=b6e3f4,c0aede,d1d4f9,ffd5dc,ffdfbf`; export const builders: Builder[] = [ - { id: "b1", name: "Priya Sharma", handle: "priya_dev", role: "Frontend Developer", avatar: AV("Priya"), country: "India", yearsExp: 3, matchScore: 92, skills: ["React", "Next.js", "TypeScript"], online: true, bio: "Loves accessible UIs and design systems." }, - { id: "b2", name: "Rahul Verma", handle: "rahul_v", role: "Full Stack Developer", avatar: AV("Rahul"), country: "India", yearsExp: 4, matchScore: 89, skills: ["Node.js", "MongoDB", "Express"], online: true, bio: "Builds end-to-end features fast." }, - { id: "b3", name: "Ankit Singh", handle: "ankit_be", role: "Backend Developer", avatar: AV("Ankit"), country: "India", yearsExp: 2, matchScore: 87, skills: ["Python", "FastAPI", "PostgreSQL"], online: false, bio: "APIs, queues and Postgres tuning." }, - { id: "b4", name: "Sneha Iyer", handle: "sneha_ux", role: "UI/UX Designer", avatar: AV("Sneha"), country: "India", yearsExp: 3, matchScore: 94, skills: ["Figma", "Adobe XD"], online: true, bio: "Product design for early-stage teams." }, - { id: "b5", name: "Vikram Mehta", handle: "vikram_fs", role: "Full Stack Dev", avatar: AV("Vikram"), country: "India", yearsExp: 4, matchScore: 93, skills: ["MERN", "Next.js"], online: false, bio: "Ships side-projects on weekends." }, - { id: "b6", name: "Aditya Rao", handle: "aditya_m", role: "Mobile Developer", avatar: AV("Aditya"), country: "India", yearsExp: 3, matchScore: 91, skills: ["Flutter", "Firebase"], online: true, bio: "Cross-platform mobile since 2021." }, - { id: "b7", name: "Sarah Chen", handle: "sarah_c", role: "ML Engineer", avatar: AV("Sarah"), country: "US", yearsExp: 5, matchScore: 88, skills: ["Python", "PyTorch", "AWS"], online: true, bio: "Recsys, embeddings, evals." }, - { id: "b8", name: "Alex Johnson", handle: "alex_j", role: "DevOps", avatar: AV("Alex"), country: "UK", yearsExp: 6, matchScore: 86, skills: ["Kubernetes", "Terraform"], online: false, bio: "Infra as code, cost optimization." }, + { + id: "b1", + name: "Priya Sharma", + handle: "priya_dev", + role: "Frontend Developer", + avatar: AV("Priya"), + country: "India", + yearsExp: 3, + matchScore: 92, + skills: ["React", "Next.js", "TypeScript"], + online: true, + bio: "Loves accessible UIs and design systems.", + }, + { + id: "b2", + name: "Rahul Verma", + handle: "rahul_v", + role: "Full Stack Developer", + avatar: AV("Rahul"), + country: "India", + yearsExp: 4, + matchScore: 89, + skills: ["Node.js", "MongoDB", "Express"], + online: true, + bio: "Builds end-to-end features fast.", + }, + { + id: "b3", + name: "Ankit Singh", + handle: "ankit_be", + role: "Backend Developer", + avatar: AV("Ankit"), + country: "India", + yearsExp: 2, + matchScore: 87, + skills: ["Python", "FastAPI", "PostgreSQL"], + online: false, + bio: "APIs, queues and Postgres tuning.", + }, + { + id: "b4", + name: "Sneha Iyer", + handle: "sneha_ux", + role: "UI/UX Designer", + avatar: AV("Sneha"), + country: "India", + yearsExp: 3, + matchScore: 94, + skills: ["Figma", "Adobe XD"], + online: true, + bio: "Product design for early-stage teams.", + }, + { + id: "b5", + name: "Vikram Mehta", + handle: "vikram_fs", + role: "Full Stack Dev", + avatar: AV("Vikram"), + country: "India", + yearsExp: 4, + matchScore: 93, + skills: ["MERN", "Next.js"], + online: false, + bio: "Ships side-projects on weekends.", + }, + { + id: "b6", + name: "Aditya Rao", + handle: "aditya_m", + role: "Mobile Developer", + avatar: AV("Aditya"), + country: "India", + yearsExp: 3, + matchScore: 91, + skills: ["Flutter", "Firebase"], + online: true, + bio: "Cross-platform mobile since 2021.", + }, + { + id: "b7", + name: "Sarah Chen", + handle: "sarah_c", + role: "ML Engineer", + avatar: AV("Sarah"), + country: "US", + yearsExp: 5, + matchScore: 88, + skills: ["Python", "PyTorch", "AWS"], + online: true, + bio: "Recsys, embeddings, evals.", + }, + { + id: "b8", + name: "Alex Johnson", + handle: "alex_j", + role: "DevOps", + avatar: AV("Alex"), + country: "UK", + yearsExp: 6, + matchScore: 86, + skills: ["Kubernetes", "Terraform"], + online: false, + bio: "Infra as code, cost optimization.", + }, ]; export const projects: Project[] = [ - { id: "p1", name: "AI Chatbot", description: "Multi-agent customer support bot for SaaS.", stack: ["React", "Node.js", "MongoDB"], owner: "Nancy Patel", members: 4, stars: 24, forks: 12, progress: 75, status: "active", icon: "🤖", language: "JavaScript", difficulty: "intermediate", remote: true, paid: true, openSource: false, ai: true, web: true, frontend: true, backend: true }, - { id: "p2", name: "AI SaaS Platform", description: "Full-stack platform with billing and dashboards.", stack: ["Next.js", "Python", "PostgreSQL"], owner: "Nancy Patel", members: 6, stars: 18, forks: 8, progress: 40, status: "active", icon: "✨", language: "Python", difficulty: "advanced", remote: true, paid: true, openSource: false, ai: true, web: true, frontend: true, backend: true }, - { id: "p3", name: "DevOps Dashboard", description: "K8s deploy monitoring with drift detection.", stack: ["Docker", "Kubernetes", "AWS"], owner: "Nancy Patel", members: 3, stars: 16, forks: 6, progress: 60, status: "active", icon: "🚀", language: "Go", difficulty: "advanced", remote: true, paid: false, openSource: false, ai: false, web: true, backend: true }, - { id: "p4", name: "Blockchain Wallet", description: "Non-custodial multi-chain wallet.", stack: ["Solidity", "Web3", "React"], owner: "Nancy Patel", members: 5, stars: 14, forks: 7, progress: 25, status: "planning", icon: "🪙", language: "TypeScript", difficulty: "advanced", remote: true, paid: false, openSource: true, ai: false, web: true, mobile: true, frontend: true }, - { id: "p5", name: "React Component Library", description: "Accessible component library with docs.", stack: ["TypeScript", "Tailwind", "Storybook"], owner: "Nancy Patel", members: 2, stars: 12, forks: 5, progress: 90, status: "active", icon: "🧩", language: "TypeScript", difficulty: "beginner", remote: true, paid: false, openSource: true, ai: false, web: true, frontend: true }, - { id: "p6", name: "Open Source CRM", description: "Lightweight CRM with pipelines and reports.", stack: ["React", "Node.js", "MongoDB"], owner: "Community", members: 8, stars: 240, forks: 96, progress: 100, status: "shipped", icon: "📇", language: "JavaScript", difficulty: "intermediate", remote: false, paid: false, openSource: true, ai: false, web: true, frontend: true, backend: true }, + { + id: "p1", + name: "AI Chatbot", + description: "Multi-agent customer support bot for SaaS.", + stack: ["React", "Node.js", "MongoDB"], + owner: "Nancy Patel", + members: 4, + stars: 24, + forks: 12, + progress: 75, + status: "active", + icon: "🤖", + language: "JavaScript", + difficulty: "intermediate", + remote: true, + paid: true, + openSource: false, + ai: true, + web: true, + frontend: true, + backend: true, + }, + { + id: "p2", + name: "AI SaaS Platform", + description: "Full-stack platform with billing and dashboards.", + stack: ["Next.js", "Python", "PostgreSQL"], + owner: "Nancy Patel", + members: 6, + stars: 18, + forks: 8, + progress: 40, + status: "active", + icon: "✨", + language: "Python", + difficulty: "advanced", + remote: true, + paid: true, + openSource: false, + ai: true, + web: true, + frontend: true, + backend: true, + }, + { + id: "p3", + name: "DevOps Dashboard", + description: "K8s deploy monitoring with drift detection.", + stack: ["Docker", "Kubernetes", "AWS"], + owner: "Nancy Patel", + members: 3, + stars: 16, + forks: 6, + progress: 60, + status: "active", + icon: "🚀", + language: "Go", + difficulty: "advanced", + remote: true, + paid: false, + openSource: false, + ai: false, + web: true, + backend: true, + }, + { + id: "p4", + name: "Blockchain Wallet", + description: "Non-custodial multi-chain wallet.", + stack: ["Solidity", "Web3", "React"], + owner: "Nancy Patel", + members: 5, + stars: 14, + forks: 7, + progress: 25, + status: "planning", + icon: "🪙", + language: "TypeScript", + difficulty: "advanced", + remote: true, + paid: false, + openSource: true, + ai: false, + web: true, + mobile: true, + frontend: true, + }, + { + id: "p5", + name: "React Component Library", + description: "Accessible component library with docs.", + stack: ["TypeScript", "Tailwind", "Storybook"], + owner: "Nancy Patel", + members: 2, + stars: 12, + forks: 5, + progress: 90, + status: "active", + icon: "🧩", + language: "TypeScript", + difficulty: "beginner", + remote: true, + paid: false, + openSource: true, + ai: false, + web: true, + frontend: true, + }, + { + id: "p6", + name: "Open Source CRM", + description: "Lightweight CRM with pipelines and reports.", + stack: ["React", "Node.js", "MongoDB"], + owner: "Community", + members: 8, + stars: 240, + forks: 96, + progress: 100, + status: "shipped", + icon: "📇", + language: "JavaScript", + difficulty: "intermediate", + remote: false, + paid: false, + openSource: true, + ai: false, + web: true, + frontend: true, + backend: true, + }, ]; export const activity: Activity[] = [ - { id: "a1", kind: "join", text: "Alex joined your project", highlight: "AI Chatbot", ago: "2m ago" }, + { + id: "a1", + kind: "join", + text: "Alex joined your project", + highlight: "AI Chatbot", + ago: "2m ago", + }, { id: "a2", kind: "accept", text: "Sarah accepted your invitation", ago: "15m ago" }, { id: "a3", kind: "commit", text: "Backend API development completed", ago: "1h ago" }, { id: "a4", kind: "merge", text: "Frontend PR #24 merged", ago: "2h ago" }, @@ -147,15 +375,66 @@ export const builderRequests: BuilderRequest[] = [ ]; export const inviteRequests: InviteRequest[] = [ - { id: "i1", project: "Open Source CRM", role: "Backend Developer", dueDays: 3, by: "Alex", icon: "📇", color: "bg-info/10 text-info" }, - { id: "i2", project: "AI SaaS Platform", role: "ML Engineer", dueDays: 5, by: "Sarah", icon: "✨", color: "bg-primary/10 text-primary" }, - { id: "i3", project: "DevOps Dashboard", role: "DevOps Engineer", dueDays: 7, by: "Mike", icon: "🚀", color: "bg-warning/10 text-warning" }, + { + id: "i1", + project: "Open Source CRM", + role: "Backend Developer", + dueDays: 3, + by: "Alex", + icon: "📇", + color: "bg-info/10 text-info", + }, + { + id: "i2", + project: "AI SaaS Platform", + role: "ML Engineer", + dueDays: 5, + by: "Sarah", + icon: "✨", + color: "bg-primary/10 text-primary", + }, + { + id: "i3", + project: "DevOps Dashboard", + role: "DevOps Engineer", + dueDays: 7, + by: "Mike", + icon: "🚀", + color: "bg-warning/10 text-warning", + }, ]; export const flares: Flare[] = [ - { id: "f1", author: builders[3], content: "Just shipped a component library refresh — new tokens, better a11y, half the CSS. AMA about migrating design systems.", tags: ["designsystems", "react"], likes: 128, comments: 22, ago: "1h ago" }, - { id: "f2", author: builders[6], content: "Wrote a small evaluator for embedding models. Cosine wasn't cutting it for our recall — dot-product + normalized inputs won.", tags: ["ml", "search"], likes: 87, comments: 14, ago: "3h ago" }, - { id: "f3", author: builders[1], content: "Anyone else notice Node 22 shaving ~10% off cold starts for our fastify APIs? Ran the same suite twice.", tags: ["node", "perf"], likes: 54, comments: 9, ago: "5h ago" }, + { + id: "f1", + author: builders[3], + content: + "Just shipped a component library refresh — new tokens, better a11y, half the CSS. AMA about migrating design systems.", + tags: ["designsystems", "react"], + likes: 128, + comments: 22, + ago: "1h ago", + }, + { + id: "f2", + author: builders[6], + content: + "Wrote a small evaluator for embedding models. Cosine wasn't cutting it for our recall — dot-product + normalized inputs won.", + tags: ["ml", "search"], + likes: 87, + comments: 14, + ago: "3h ago", + }, + { + id: "f3", + author: builders[1], + content: + "Anyone else notice Node 22 shaving ~10% off cold starts for our fastify APIs? Ran the same suite twice.", + tags: ["node", "perf"], + likes: 54, + comments: 9, + ago: "5h ago", + }, ]; export const conversations: Conversation[] = [ @@ -178,19 +457,61 @@ export const notifications: Notification[] = [ { id: "n2", kind: "comment", text: "Sarah commented on your post", ago: "15m ago", unread: true }, { id: "n3", kind: "invite", text: "Your invitation was accepted", ago: "1h ago", unread: false }, { id: "n4", kind: "match", text: "New builder matches found", ago: "3h ago", unread: false }, - { id: "n5", kind: "hackathon", text: "Hackathon deadline reminder", ago: "1d ago", unread: false }, + { + id: "n5", + kind: "hackathon", + text: "Hackathon deadline reminder", + ago: "1d ago", + unread: false, + }, ]; export const hackathons: Hackathon[] = [ - { id: "h1", name: "AI for Good 2025", theme: "Social impact", startsIn: 12, prize: "$25k", teamSize: "2–5", registered: true }, - { id: "h2", name: "DevLink Winter Jam", theme: "Any theme", startsIn: 30, prize: "$10k", teamSize: "1–4", registered: false }, - { id: "h3", name: "Chain Builders", theme: "Web3", startsIn: 45, prize: "$50k", teamSize: "1–5", registered: false }, + { + id: "h1", + name: "AI for Good 2025", + theme: "Social impact", + startsIn: 12, + prize: "$25k", + teamSize: "2–5", + registered: true, + }, + { + id: "h2", + name: "DevLink Winter Jam", + theme: "Any theme", + startsIn: 30, + prize: "$10k", + teamSize: "1–4", + registered: false, + }, + { + id: "h3", + name: "Chain Builders", + theme: "Web3", + startsIn: 45, + prize: "$50k", + teamSize: "1–5", + registered: false, + }, ]; export const deadlines: Deadline[] = [ { id: "d1", project: "AI Chatbot", milestone: "Backend API", dueDays: 2, severity: "danger" }, - { id: "d2", project: "DevOps Dashboard", milestone: "Deployment", dueDays: 5, severity: "warning" }, - { id: "d3", project: "Hackathon 2025", milestone: "Registration", dueDays: 7, severity: "warning" }, + { + id: "d2", + project: "DevOps Dashboard", + milestone: "Deployment", + dueDays: 5, + severity: "warning", + }, + { + id: "d3", + project: "Hackathon 2025", + milestone: "Registration", + dueDays: 7, + severity: "warning", + }, { id: "d4", project: "Project Milestone", milestone: "v1.0", dueDays: 10, severity: "info" }, ]; diff --git a/frontend/src/routes/README.md b/frontend/src/routes/README.md index 441a4e82..0990d2f9 100644 --- a/frontend/src/routes/README.md +++ b/frontend/src/routes/README.md @@ -7,15 +7,15 @@ is `src/routes/__root.tsx`. ## Conventions -| File | URL | -| --- | --- | -| `index.tsx` | `/` | -| `about.tsx` | `/about` | -| `users/index.tsx` | `/users` | -| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) | -| `posts/{-$category}.tsx` | `/posts/:category?` (optional segment) | -| `files/$.tsx` | `/files/*` (splat — read via `_splat` param, never `*`) | -| `_layout.tsx` | layout route (renders children via ``) | -| `__root.tsx` | app shell — wraps every page; preserve `` | +| File | URL | +| ------------------------ | ------------------------------------------------------- | +| `index.tsx` | `/` | +| `about.tsx` | `/about` | +| `users/index.tsx` | `/users` | +| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) | +| `posts/{-$category}.tsx` | `/posts/:category?` (optional segment) | +| `files/$.tsx` | `/files/*` (splat — read via `_splat` param, never `*`) | +| `_layout.tsx` | layout route (renders children via ``) | +| `__root.tsx` | app shell — wraps every page; preserve `` | `routeTree.gen.ts` is auto-generated. Don't edit it by hand. diff --git a/frontend/src/routes/_app.analytics.tsx b/frontend/src/routes/_app.analytics.tsx index c05eb6ae..07d62d1a 100644 --- a/frontend/src/routes/_app.analytics.tsx +++ b/frontend/src/routes/_app.analytics.tsx @@ -2,8 +2,15 @@ import { createFileRoute } from "@tanstack/react-router"; import { Card } from "@/components/shared/primitives"; import { stats } from "@/mocks/seed"; import { - LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, - BarChart, Bar, CartesianGrid, + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, + CartesianGrid, } from "recharts"; export const Route = createFileRoute("/_app/analytics")({ @@ -35,7 +42,9 @@ function AnalyticsPage() {
        {stats.slice(0, 4).map((s) => ( -

        {s.label}

        +

        + {s.label} +

        {s.value}

        ))} @@ -49,8 +58,21 @@ function AnalyticsPage() { - - + +
        @@ -63,7 +85,14 @@ function AnalyticsPage() { - + diff --git a/frontend/src/routes/_app.bookmarks.tsx b/frontend/src/routes/_app.bookmarks.tsx index d48b16fc..330866c8 100644 --- a/frontend/src/routes/_app.bookmarks.tsx +++ b/frontend/src/routes/_app.bookmarks.tsx @@ -21,21 +21,29 @@ function BookmarksPage() {

        Everything you've saved.

    -

    Projects

    +

    + Projects +

    {projects.slice(0, 3).map((p) => (
    - {p.icon} + + {p.icon} +

    {p.name}

    -

    {p.description}

    +

    + {p.description} +

    - {p.stack.map((s) => {s})} + {p.stack.map((s) => ( + {s} + ))}
    @@ -43,7 +51,9 @@ function BookmarksPage() {
    -

    Flares

    +

    + Flares +

    {flares.slice(0, 2).map((f) => ( diff --git a/frontend/src/routes/_app.builders.$builderId.tsx b/frontend/src/routes/_app.builders.$builderId.tsx index b8dbe26f..28d0543c 100644 --- a/frontend/src/routes/_app.builders.$builderId.tsx +++ b/frontend/src/routes/_app.builders.$builderId.tsx @@ -7,31 +7,43 @@ import { BackButton } from "@/components/shared/BackButton"; export const Route = createFileRoute("/_app/builders/$builderId")({ head: ({ params }) => ({ - meta: [{ title: `Builder — DevLink`, }, { name: "description", content: `Builder ${params.builderId} on DevLink.` }], + meta: [ + { title: `Builder — DevLink` }, + { name: "description", content: `Builder ${params.builderId} on DevLink.` }, + ], }), component: BuilderProfile, }); function BuilderProfile() { const { builderId } = Route.useParams(); - const { data: b, isLoading } = useQuery({ queryKey: ["builder", builderId], queryFn: () => buildersService.get(builderId) }); + const { data: b, isLoading } = useQuery({ + queryKey: ["builder", builderId], + queryFn: () => buildersService.get(builderId), + }); if (isLoading) return ; if (!b) throw notFound(); return (
    - + + Back to builders +

    {b.name}

    -

    @{b.handle} · {b.role}

    +

    + @{b.handle} · {b.role} +

    {b.bio}

    - {b.skills.map((s) => {s})} + {b.skills.map((s) => ( + {s} + ))}
    @@ -45,9 +57,20 @@ function BuilderProfile() {
    -

    Match Score

    {b.matchScore}%

    -

    Experience

    {b.yearsExp} yrs

    -

    Location

    {b.country}

    + +

    Match Score

    +

    {b.matchScore}%

    +
    + +

    Experience

    +

    + {b.yearsExp} yrs +

    +
    + +

    Location

    +

    {b.country}

    +
    ); diff --git a/frontend/src/routes/_app.builders.tsx b/frontend/src/routes/_app.builders.tsx index 8fdec8a2..34ce38b4 100644 --- a/frontend/src/routes/_app.builders.tsx +++ b/frontend/src/routes/_app.builders.tsx @@ -10,7 +10,10 @@ export const Route = createFileRoute("/_app/builders")({ head: () => ({ meta: [ { title: "Builders — DevLink" }, - { name: "description", content: "Discover developers by skills, match score and availability." }, + { + name: "description", + content: "Discover developers by skills, match score and availability.", + }, ], }), component: BuildersPage, @@ -19,10 +22,15 @@ export const Route = createFileRoute("/_app/builders")({ function BuildersPage() { const [tab, setTab] = useState<"discover" | "matches" | "connections">("discover"); const [q, setQ] = useState(""); - const { data = [] } = useQuery({ queryKey: ["builders", tab], queryFn: tab === "matches" ? buildersService.matches : buildersService.list }); + const { data = [] } = useQuery({ + queryKey: ["builders", tab], + queryFn: tab === "matches" ? buildersService.matches : buildersService.list, + }); const filtered = data.filter( - (b) => b.name.toLowerCase().includes(q.toLowerCase()) || b.skills.some((s) => s.toLowerCase().includes(q.toLowerCase())), + (b) => + b.name.toLowerCase().includes(q.toLowerCase()) || + b.skills.some((s) => s.toLowerCase().includes(q.toLowerCase())), ); const tabs = [ @@ -46,7 +54,9 @@ function BuildersPage() { onClick={() => setTab(t.k)} className={cn( "rounded px-2.5 py-1 text-[12px] font-medium transition-colors", - tab === t.k ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground", + tab === t.k + ? "bg-primary text-primary-foreground" + : "text-muted-foreground hover:text-foreground", )} > {t.label} @@ -54,7 +64,10 @@ function BuildersPage() { ))}
    - + setQ(e.target.value)} @@ -73,14 +86,22 @@ function BuildersPage() {

    {b.name}

    {b.role}

    -

    {b.country} · {b.yearsExp} yrs

    +

    + {b.country} · {b.yearsExp} yrs +

    - {b.skills.slice(0, 3).map((s) => {s})} + {b.skills.slice(0, 3).map((s) => ( + {s} + ))}

    {b.matchScore}% Match

    - - + +
    diff --git a/frontend/src/routes/_app.dashboard.tsx b/frontend/src/routes/_app.dashboard.tsx index 6ddc9242..06b5aaf7 100644 --- a/frontend/src/routes/_app.dashboard.tsx +++ b/frontend/src/routes/_app.dashboard.tsx @@ -18,7 +18,10 @@ export const Route = createFileRoute("/_app/dashboard")({ head: () => ({ meta: [ { title: "Dashboard — DevLink" }, - { name: "description", content: "Your DevLink command center: projects, matches, messages and streaks." }, + { + name: "description", + content: "Your DevLink command center: projects, matches, messages and streaks.", + }, ], }), component: Dashboard, diff --git a/frontend/src/routes/_app.flares.tsx b/frontend/src/routes/_app.flares.tsx index 6902d18c..3913f529 100644 --- a/frontend/src/routes/_app.flares.tsx +++ b/frontend/src/routes/_app.flares.tsx @@ -65,7 +65,10 @@ function FlaresPage() {

    Markdown supported

    -
    - -
    {f.tags.map((t) => ( #{t} ))}
    -
    -
    - - -
    - {/* Placeholder: ApplyButton needs flareId+projectId; current flares feed is mock-only. */} +
    + +
    diff --git a/frontend/src/routes/_app.hackathons.tsx b/frontend/src/routes/_app.hackathons.tsx index 0a2b0996..c7816a52 100644 --- a/frontend/src/routes/_app.hackathons.tsx +++ b/frontend/src/routes/_app.hackathons.tsx @@ -20,7 +20,9 @@ function HackathonsPage() {

    Hackathons

    -

    Join a jam, build a team, ship something new.

    +

    + Join a jam, build a team, ship something new. +

    {data.map((h) => ( @@ -38,8 +40,12 @@ function HackathonsPage() {

    {h.name}

    {h.theme}

    - Starts in {h.startsIn}d - {h.teamSize} + + Starts in {h.startsIn}d + + + {h.teamSize} + {h.prize}
    {data.length === 0 && ( -

    No messages yet — say hello 👋

    +

    + No messages yet — say hello 👋 +

    )} {data.map((m) => ( -
    +

    {m.text}

    -

    {m.at}

    +

    + {m.at} +

    ))}
    { e.preventDefault(); setText(""); }} + onSubmit={(e) => { + e.preventDefault(); + setText(""); + }} className="flex items-center gap-2 border-t border-border p-3" > @@ -32,7 +35,9 @@ function MessagesIndex() { >
    -

    {c.with.name}

    +

    + {c.with.name} +

    {c.preview}

    @@ -52,7 +57,9 @@ function MessagesIndex() {

    Select a conversation

    -

    Choose a chat on the left to start messaging.

    +

    + Choose a chat on the left to start messaging. +

    diff --git a/frontend/src/routes/_app.notifications.tsx b/frontend/src/routes/_app.notifications.tsx index 95991e19..f514641e 100644 --- a/frontend/src/routes/_app.notifications.tsx +++ b/frontend/src/routes/_app.notifications.tsx @@ -15,7 +15,10 @@ export const Route = createFileRoute("/_app/notifications")({ }); function NotificationsPage() { - const { data = [] } = useQuery({ queryKey: ["notifications"], queryFn: notificationsService.list }); + const { data = [] } = useQuery({ + queryKey: ["notifications"], + queryFn: notificationsService.list, + }); return (
    @@ -30,8 +33,16 @@ function NotificationsPage() {
      {data.map((n) => ( -
    • - +
    • +

      {n.text}

      {n.ago}
    • diff --git a/frontend/src/routes/_app.profile.$username.tsx b/frontend/src/routes/_app.profile.$username.tsx index 25b0bb0c..a48b9ae0 100644 --- a/frontend/src/routes/_app.profile.$username.tsx +++ b/frontend/src/routes/_app.profile.$username.tsx @@ -7,7 +7,10 @@ export const Route = createFileRoute("/_app/profile/$username")({ head: ({ params }) => ({ meta: [ { title: `@${params.username} — DevLink` }, - { name: "description", content: `${params.username}'s DevLink profile: skills, projects and activity.` }, + { + name: "description", + content: `${params.username}'s DevLink profile: skills, projects and activity.`, + }, ], }), component: ProfilePage, @@ -17,7 +20,14 @@ function ProfilePage() { const { username } = Route.useParams(); const me = username === currentUser.handle; const b = me - ? { ...builders[0], name: currentUser.name, handle: currentUser.handle, avatar: currentUser.avatar, bio: "Product engineer. Ships fast, sleeps sometimes.", role: "Full Stack Developer" } + ? { + ...builders[0], + name: currentUser.name, + handle: currentUser.handle, + avatar: currentUser.avatar, + bio: "Product engineer. Ships fast, sleeps sometimes.", + role: "Full Stack Developer", + } : builders.find((x) => x.handle === username); if (!b) throw notFound(); @@ -28,12 +38,20 @@ function ProfilePage() {

      {b.name}

      -

      @{b.handle} · {b.role}

      +

      + @{b.handle} · {b.role} +

      {b.bio}

      - {b.country} - Joined 2024 - devlink.io/{b.handle} + + {b.country} + + + Joined 2024 + + + devlink.io/{b.handle} +
      {!me && ( @@ -48,7 +66,9 @@ function ProfilePage() {

      Skills

      - {b.skills.map((s) => {s})} + {b.skills.map((s) => ( + {s} + ))}
      @@ -56,10 +76,14 @@ function ProfilePage() {
        {projects.slice(0, 4).map((p) => (
      • - {p.icon} + + {p.icon} +

        {p.name}

        -

        {p.stack.join(" · ")}

        +

        + {p.stack.join(" · ")} +

      • ))} diff --git a/frontend/src/routes/_app.projects.tsx b/frontend/src/routes/_app.projects.tsx index 1923b714..45b9d8c8 100644 --- a/frontend/src/routes/_app.projects.tsx +++ b/frontend/src/routes/_app.projects.tsx @@ -18,7 +18,16 @@ export const Route = createFileRoute("/_app/projects")({ const LANGUAGES = ["JavaScript", "TypeScript", "Python", "Go", "Rust", "Java", "C++"]; const DIFFICULTIES = ["beginner", "intermediate", "advanced"] as const; -const BOOL_FILTERS = ["remote", "paid", "openSource", "ai", "web", "mobile", "backend", "frontend"] as const; +const BOOL_FILTERS = [ + "remote", + "paid", + "openSource", + "ai", + "web", + "mobile", + "backend", + "frontend", +] as const; type BoolFilter = (typeof BOOL_FILTERS)[number]; const BOOL_LABELS: Record = { @@ -62,13 +71,18 @@ function toggle(set: T[], val: T): T[] { function ProjectsPage() { const [q, setQ] = useState(""); - const [statusFilter, setStatusFilter] = useState<"all" | "recruiting" | "in-progress" | "completed" | "archived">("all"); + const [statusFilter, setStatusFilter] = useState<"all" | "active" | "planning" | "shipped">( + "all", + ); const [showFilters, setShowFilters] = useState(false); const [langs, setLangs] = useState([]); const [difficulties, setDifficulties] = useState([]); const [boolFilters, setBoolFilters] = useState([]); - const { data = [], isLoading } = useQuery({ queryKey: ["projects"], queryFn: projectsService.list }); + const { data = [], isLoading } = useQuery({ + queryKey: ["projects"], + queryFn: projectsService.list, + }); const hasActiveFilters = langs.length > 0 || difficulties.length > 0 || boolFilters.length > 0; @@ -82,7 +96,8 @@ function ProjectsPage() { if (statusFilter !== "all" && p.status !== statusFilter) return false; if (q && !p.name.toLowerCase().includes(q.toLowerCase())) return false; if (langs.length > 0 && (!p.language || !langs.includes(p.language))) return false; - if (difficulties.length > 0 && (!p.difficulty || !difficulties.includes(p.difficulty))) return false; + if (difficulties.length > 0 && (!p.difficulty || !difficulties.includes(p.difficulty))) + return false; for (const f of boolFilters) { if (!p[f]) return false; } @@ -94,7 +109,9 @@ function ProjectsPage() {

        Projects

        -

        Everything you're building, in one place.

        +

        + Everything you're building, in one place. +

        )} @@ -217,13 +266,22 @@ function ProjectsPage() { ) : (
        {filtered.map((p) => ( - +
        - {p.icon} + + {p.icon} +

        {p.name}

        -

        {p.description}

        +

        + {p.description} +

        @@ -231,11 +289,15 @@ function ProjectsPage() { {s} ))} {p.difficulty && ( - + {p.difficulty} )} @@ -259,18 +321,17 @@ function ProjectsPage() { {p.forks} - {p.status - .split("-") - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ")} + + {p.status} +
        diff --git a/frontend/src/routes/_app.search.tsx b/frontend/src/routes/_app.search.tsx index b444de70..b4f7fdd0 100644 --- a/frontend/src/routes/_app.search.tsx +++ b/frontend/src/routes/_app.search.tsx @@ -12,7 +12,10 @@ export const Route = createFileRoute("/_app/search")({ head: () => ({ meta: [ { title: "Search — DevLink" }, - { name: "description", content: "Global search across developers, projects, skills and flares." }, + { + name: "description", + content: "Global search across developers, projects, skills and flares.", + }, ], }), component: SearchPage, @@ -22,38 +25,32 @@ function SearchPage() { const [q, setQ] = useState(""); const [tab, setTab] = useState("Developers"); - const devs = builders.filter((b) => (b.name + b.skills.join(" ")).toLowerCase().includes(q.toLowerCase())); - const projs = projects.filter((p) => (p.name + p.stack.join(" ")).toLowerCase().includes(q.toLowerCase())); - const skillSet = Array.from(new Set(builders.flatMap((b) => b.skills))).filter((s) => s.toLowerCase().includes(q.toLowerCase())); + const devs = builders.filter((b) => + (b.name + b.skills.join(" ")).toLowerCase().includes(q.toLowerCase()), + ); + const projs = projects.filter((p) => + (p.name + p.stack.join(" ")).toLowerCase().includes(q.toLowerCase()), + ); + const skillSet = Array.from(new Set(builders.flatMap((b) => b.skills))).filter((s) => + s.toLowerCase().includes(q.toLowerCase()), + ); const fls = flares.filter((f) => f.content.toLowerCase().includes(q.toLowerCase())); return (
        - - - setQ(e.target.value)} - placeholder="Search DevLink…" - className="w-full rounded-md border border-border bg-surface py-2.5 pl-10 pr-10 text-[14px] outline-none focus:border-primary focus:ring-2 focus:ring-primary/20" - autoFocus - /> - - {q && ( - - )} -
        + + setQ(e.target.value)} + placeholder="Search DevLink…" + className="w-full rounded-md border border-border bg-surface py-2.5 pl-10 pr-3 text-[14px] outline-none focus:border-primary focus:ring-2 focus:ring-primary/20" + autoFocus + /> +
        {tabs.map((t) => (