diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ecbf18ca..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,25 +0,0 @@ -# Contributing to this Wiki - -This is a wiki for [Talon](https://talonvoice.com/), a hands-free input replacement for using a computer. These docs are rendered and deployed to: - -[https://talon.wiki/](https://talon.wiki) - -Join the [Talon Slack](https://talonvoice.com/chat) to find other folks interested in or using Talon. If you want to support the project, consider donating to the [Patreon](https://www.patreon.com/lunixbochs). - -## Who can Contribute - -This wiki belongs to the Talon Community, and contributions are welcome from anyone. - -## Build and view changes locally - -Install `npm` then run the following - -``` -npm i -npm start -``` - -## Checklist - -- Use relative markdown links -- run `npm run build` and make sure the build was successful (i.e. you didn't break any links) diff --git a/README.md b/README.md index bd126af4..4205e4dc 100644 --- a/README.md +++ b/README.md @@ -1 +1,136 @@ -This is the source repo for the [Talon wiki](https://talon.wiki), a community maintained wiki for [Talon voice](https://talonvoice.com/). For information on how to contribute, see our [Contributing](https://talon.wiki/CONTRIBUTING/) documentation. +# Talon Wiki + +This is the source repo for the [https://talon.wiki/](https://talon.wiki), a community maintained wiki for [Talon voice](https://talonvoice.com/). + +Join the [Talon Slack](https://talonvoice.com/chat) to find other folks interested in or using Talon. If you want to support the project, consider donating to the [Patreon](https://www.patreon.com/lunixbochs). + +## Who can Contribute + +This wiki belongs to the Talon Community, and contributions are welcome from anyone. + +## Development Setup for Contributors + +### Prerequisites + +- [Node.js](https://nodejs.org/) (version 18 or higher) +- npm (comes with Node.js) +- A GitHub account + +### Getting Started + +1. **Fork the repository** on GitHub by clicking the "Fork" button at the top of the [TalonCommunity/Wiki](https://github.com/TalonCommunity/Wiki) repository page. + +2. **Clone your fork** (replace `YOUR_USERNAME` with your GitHub username): + + ```bash + git clone https://github.com/YOUR_USERNAME/Wiki.git + cd Wiki + ``` + +3. **Add the upstream remote** to keep your fork in sync: + + ```bash + git remote add upstream https://github.com/TalonCommunity/Wiki.git + ``` + +4. **Install dependencies**: + + ```bash + npm install + ``` + +5. **Start the development server**: + ```bash + npm start + ``` + +The site will open at `http://localhost:3000` and automatically reload when you make changes. + +### Contributing Workflow + +1. **Create a new branch** for your changes: + + ```bash + git checkout -b your-feature-branch + ``` + +2. **Make your changes** and test them locally + +3. **Commit your changes**: + + ```bash + git add . + git commit -m "Description of your changes" + ``` + +4. **Push to your fork**: + + ```bash + git push origin your-feature-branch + ``` + +5. **Create a Pull Request** from your fork to the main repository + +### Keeping Your Fork Updated + +Before starting new work, sync your fork with the upstream repository: + +```bash +git checkout main +git fetch upstream +git merge upstream/main +git push origin main +``` + +### Checklist for Contributors + +- [ ] Fork the repository and create a feature branch +- [ ] Use relative markdown links +- [ ] Run `npm run build` and ensure the build is successful (no broken links) +- [ ] Test your changes locally before submitting a pull request +- [ ] Create a pull request with a clear description of your changes + +## Available Commands + +| Command | Description | +| ---------------------- | ----------------------------------------------- | +| `npm start` | Start the development server with hot reloading | +| `npm run build` | Build the static site for production | +| `npm run serve` | Serve the production build locally | +| `npm run update-repos` | Fetch fresh repository data from GitHub API | +| `npm run clear` | Clear Docusaurus cache | +| `npm run typecheck` | Run TypeScript type checking | + +## Repository Explorer + +The wiki includes a [Repository Explorer](https://talon.wiki/explorer/) that automatically displays Talon-related repositories from GitHub tagged with `talonvoice` topic. + +The repository data is cached locally to avoid hitting GitHub's API rate limits (60 requests per hour for unauthenticated users, 5,000 for authenticated users). To update the cached data, you can run: + +```bash +npm run update-repos +# or +npm run build --fetch-repos +``` + +Otherwise, the explorer will use the cached data until the next production build or scheduled update. + +### Omitting Repositories from the Explorer + +To exclude specific repositories from the explorer: + +1. Edit `plugins/repo-data-omit-list.json` +2. Add repository full names to the `omitRepos` array: + ```json + { + "omitRepos": ["owner/repo-name", "another-owner/another-repo"] + } + ``` +3. Changes take effect on the next build + +## Technology Stack + +- [Docusaurus](https://docusaurus.io/) - Static site generator +- [React](https://reactjs.org/) - Component framework +- [TypeScript](https://www.typescriptlang.org/) - Type safety +- [Shiki](https://shiki.style/) - Syntax highlighting diff --git a/docs/Integrations/talon_user_file_sets.md b/docs/Integrations/talon_user_file_sets.md index f5b4dd30..27feed4f 100644 --- a/docs/Integrations/talon_user_file_sets.md +++ b/docs/Integrations/talon_user_file_sets.md @@ -5,7 +5,7 @@ The main Talon user file set for Talon is [Talon Community](https://github.com/t If intended for public consumption, these Talon user file sets are mostly annouced via the [Slack channel](https://talonvoice.com/chat). Aside from that there are a few ways you can discover them: - You can make use of the [Talon code search](https://search.talonvoice.com/search/). This aims to search all known github repositories containing Talon related code. If you're looking for integration with a particular application this is a good option. -- You can browse the [github talonvoice topic](https://github.com/topics/talonvoice). Repositories can optionally tag themselves with this to aid discoverability. +- You can browse the [Repository Explorer](/explorer) for Talon user file sets tagged with `talonvoice`. Repositories can tag themselves with this to aid discoverability. - You can take a look at the manually curated list below. ## Voice controlled hands free mouse replacements diff --git a/docusaurus.config.ts b/docusaurus.config.ts index f2bfec37..8b242035 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -69,6 +69,7 @@ const config: Config = { } satisfies Preset.Options, ], ], + plugins: ["./plugins/repo-data-plugin.js"], markdown: { mermaid: true, }, @@ -145,6 +146,11 @@ const config: Config = { position: "left", label: "Resource Hub", }, + { + to: "/explorer", + position: "left", + label: "Repository Explorer", + }, { type: "docSidebar", sidebarId: "HelpSidebar", diff --git a/package-lock.json b/package-lock.json index a1d0d32d..fb6f22cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mdx-js/react": "^3.0.0", "@shikijs/rehype": "^3.1.0", "clsx": "^2.0.0", + "node-fetch": "^2.7.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -26,6 +27,7 @@ "@docusaurus/types": "^3.7.0", "@shikijs/colorized-brackets": "^3.1.0", "@shikijs/transformers": "^3.1.0", + "cross-env": "^7.0.3", "typescript": "~5.2.2" }, "engines": { @@ -7085,6 +7087,24 @@ } } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -13530,6 +13550,25 @@ "node": ">=18" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17785,6 +17824,11 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -18448,6 +18492,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "5.98.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", @@ -18797,6 +18846,15 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ec8dfe54..4bc4221b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build && cp _redirects build/_redirects", + "update-repos": "cross-env FETCH_REPOS=true npm run build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -22,6 +23,7 @@ "@mdx-js/react": "^3.0.0", "@shikijs/rehype": "^3.1.0", "clsx": "^2.0.0", + "node-fetch": "^2.7.0", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -33,6 +35,7 @@ "@docusaurus/types": "^3.7.0", "@shikijs/colorized-brackets": "^3.1.0", "@shikijs/transformers": "^3.1.0", + "cross-env": "^7.0.3", "typescript": "~5.2.2" }, "browserslist": { diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json new file mode 100644 index 00000000..eaf34965 --- /dev/null +++ b/plugins/repo-data-omit-list.json @@ -0,0 +1,4 @@ +{ + "description": "List of repositories to exclude from the Repository Explorer. Add repository full names (e.g., 'owner/repo-name') to the omitRepos array.", + "omitRepos": ["voqal/voqal"] +} diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js new file mode 100644 index 00000000..c0a87c45 --- /dev/null +++ b/plugins/repo-data-plugin.js @@ -0,0 +1,183 @@ +const fetch = require("node-fetch"); +const fs = require("fs"); +const path = require("path"); + +module.exports = function (context, options) { + return { + name: "repo-data-plugin", + async loadContent() { + const cacheFile = path.join( + __dirname, + "../.docusaurus/repo-data-plugin/default/repos.json", + ); + + // Load omit configuration from repo-explorer-omit-list.json + let omitRepos = []; + + try { + const omitListFile = path.join(__dirname, "repo-data-omit-list.json"); + if (fs.existsSync(omitListFile)) { + const omitConfig = JSON.parse(fs.readFileSync(omitListFile, "utf8")); + if (omitConfig.omitRepos && Array.isArray(omitConfig.omitRepos)) { + omitRepos = omitConfig.omitRepos; + } + } + } catch (error) { + console.warn("Failed to load repo-data-omit-list.json:", error.message); + } + + // Remove duplicates and normalize + omitRepos = [...new Set(omitRepos.map((repo) => repo.toLowerCase()))]; + + if (omitRepos.length > 0) { + console.log( + `Repository omit list loaded: ${omitRepos.length} repositories will be excluded`, + ); + } // Determine if we should fetch fresh data + const isUpdateRepos = + (process.env.npm_config_argv && + JSON.parse(process.env.npm_config_argv).original.includes( + "update-repos", + )) || // npm run update-repos + process.env.npm_lifecycle_event === "update-repos" || // Direct npm run update-repos + process.argv.some((arg) => arg.includes("update-repos")) || // Command line contains update-repos + process.argv.includes("--fetch-repos"); // Explicit command line flag + + const shouldFetch = + isUpdateRepos || // Explicit update request + process.env.FETCH_REPOS === "true" || // Environment variable + process.env.GITHUB_REF === "refs/heads/main" || // Main branch deployment + (process.env.CI === "true" && + process.env.GITHUB_EVENT_NAME === "schedule") || // Scheduled CI + !fs.existsSync(cacheFile); // No cached data exists // Try to use cached data if we shouldn't fetch + if (!shouldFetch) { + console.log("Using cached repository data (no fresh fetch needed)"); + try { + if (fs.existsSync(cacheFile)) { + console.log( + "Using cached repository data (add --fetch-repos to update)", + ); + const cachedData = JSON.parse(fs.readFileSync(cacheFile, "utf8")); + + // Apply current omit list to cached data + const filteredRepos = cachedData.repositories.filter((repo) => { + const fullName = repo.full_name.toLowerCase(); + return !omitRepos.includes(fullName); + }); + + console.log( + `Loaded ${filteredRepos.length} repositories from cache (generated at ${cachedData.generated_at})`, + ); + if (filteredRepos.length !== cachedData.repositories.length) { + console.log( + `Applied current omit list: ${cachedData.repositories.length - filteredRepos.length} repositories filtered out`, + ); + } + + return { + ...cachedData, + repositories: filteredRepos, + filtered_count: filteredRepos.length, + omitted_count: + (cachedData.repositories.length || 0) - filteredRepos.length, + }; + } + } catch (error) { + console.warn( + "Failed to read cached data, falling back to fresh fetch:", + error.message, + ); + } + } + + try { + console.log("Fetching fresh Talon repositories from GitHub..."); + const response = await fetch( + "https://api.github.com/search/repositories?q=topic:talonvoice&sort=stars&order=desc&per_page=100", + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "TalonWiki/1.0", + // Add GitHub token if available for higher rate limits + ...(process.env.GITHUB_TOKEN && { + Authorization: `token ${process.env.GITHUB_TOKEN}`, + }), + }, + }, + ); + + if (!response.ok) { + throw new Error( + `GitHub API error: ${response.status} ${response.statusText}`, + ); + } + + const data = await response.json(); + console.log(`Successfully fetched ${data.items.length} repositories`); + + // Filter out omitted repositories + const filteredRepos = data.items.filter((repo) => { + const fullName = repo.full_name.toLowerCase(); + const isOmitted = omitRepos.includes(fullName); + if (isOmitted) { + console.log(`Omitting repository: ${repo.full_name}`); + } + return !isOmitted; + }); + + console.log( + `After filtering: ${filteredRepos.length} repositories (${data.items.length - filteredRepos.length} omitted)`, + ); + + return { + repositories: filteredRepos, + total_count: data.total_count, + filtered_count: filteredRepos.length, + omitted_count: data.items.length - filteredRepos.length, + generated_at: new Date().toISOString(), + }; + } catch (error) { + console.error("Failed to fetch repository data:", error); + + // Try to use cached data as fallback + try { + if (fs.existsSync(cacheFile)) { + console.warn("Using cached data as fallback..."); + const cachedData = JSON.parse(fs.readFileSync(cacheFile, "utf8")); + + // Apply current omit list to fallback cached data + const filteredRepos = cachedData.repositories.filter((repo) => { + const fullName = repo.full_name.toLowerCase(); + return !omitRepos.includes(fullName); + }); + + return { + ...cachedData, + repositories: filteredRepos, + filtered_count: filteredRepos.length, + omitted_count: + (cachedData.repositories.length || 0) - filteredRepos.length, + error: `Build-time fetch failed: ${error.message}. Using cached data.`, + }; + } + } catch (cacheError) { + console.error("Failed to read cached data:", cacheError); + } + + // Return empty data instead of failing the build + return { + repositories: [], + total_count: 0, + generated_at: new Date().toISOString(), + error: error.message, + }; + } + }, + + async contentLoaded({ content, actions }) { + const { createData } = actions; + // Create a JSON file that can be imported by components + await createData("repos.json", JSON.stringify(content)); + }, + }; +}; diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx new file mode 100644 index 00000000..a78fe5ea --- /dev/null +++ b/src/components/RepoExplorer/index.tsx @@ -0,0 +1,521 @@ +import React, { useState, useEffect } from "react"; +import clsx from "clsx"; +import styles from "./styles.module.css"; +// @ts-ignore +import repoData from "@generated/repo-data-plugin/default/repos.json"; + +interface GitHubRepo { + id: number; + name: string; + full_name: string; + description: string | null; + html_url: string; + stargazers_count: number; + language: string | null; + updated_at: string; + topics: string[]; + owner: { + login: string; + avatar_url: string; + }; +} + +interface RepoData { + repositories: GitHubRepo[]; + total_count: number; + generated_at: string; + error?: string; +} + +const RepoExplorer: React.FC = () => { + const [repos, setRepos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedLanguage, setSelectedLanguage] = useState("all"); + const [selectedTags, setSelectedTags] = useState([]); + const [sortBy, setSortBy] = useState<"stars" | "updated" | "name">("stars"); + const [viewMode, setViewMode] = useState<"compact" | "expanded">("compact"); + const [showAllTags, setShowAllTags] = useState(false); + + useEffect(() => { + // Load data from build-time generated JSON + try { + const data: RepoData = repoData; + + if (data.error) { + setError(`Build-time data fetch failed: ${data.error}`); + } else { + setRepos(data.repositories); + console.log( + `Loaded ${data.repositories.length} repositories (generated at ${data.generated_at})`, + ); + } + } catch (err) { + setError("Failed to load repository data"); + console.error("Error loading repo data:", err); + } finally { + setLoading(false); + } + }, []); + + const filteredAndSortedRepos = React.useMemo(() => { + let filtered = repos.filter((repo) => { + const matchesSearch = + repo.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (repo.description?.toLowerCase().includes(searchTerm.toLowerCase()) ?? + false) || + repo.owner.login.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesLanguage = + selectedLanguage === "all" || repo.language === selectedLanguage; + const matchesTags = + selectedTags.length === 0 || + selectedTags.some((tag) => repo.topics.includes(tag)); + return matchesSearch && matchesLanguage && matchesTags; + }); // Sort repositories + filtered.sort((a, b) => { + switch (sortBy) { + case "stars": + return b.stargazers_count - a.stargazers_count; + case "updated": + return ( + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ); + case "name": + return a.name.localeCompare(b.name); + default: + return 0; + } + }); + + return filtered; + }, [repos, searchTerm, selectedLanguage, selectedTags, sortBy]); + + const languages = React.useMemo(() => { + return Array.from( + new Set(repos.map((repo) => repo.language).filter(Boolean)), + ).sort(); + }, [repos]); + + // Get all unique tags with counts, excluding 'talonvoice' and 'talon' + const tagStats = React.useMemo(() => { + const tagCounts = new Map(); + repos.forEach((repo) => { + repo.topics + .filter((topic) => topic !== "talonvoice" && topic !== "talon") + .forEach((topic) => { + tagCounts.set(topic, (tagCounts.get(topic) || 0) + 1); + }); + }); + + return Array.from(tagCounts.entries()) + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count); // Sort by count descending + }, [repos]); + + const toggleTag = (tag: string) => { + setSelectedTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], + ); + }; + + const clearAllFilters = () => { + setSearchTerm(""); + setSelectedLanguage("all"); + setSelectedTags([]); + }; + + const formatDate = (dateString: string) => { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(dateString)); + }; + const getLanguageColor = (language: string | null): string => { + if (!language) return "#858585"; + + const colors: Record = { + Python: "#3776ab", + JavaScript: "#f7df1e", + TypeScript: "#3178c6", + Shell: "#4ade80", // Better contrast green + Go: "#00add8", + Rust: "#dea584", + HTML: "#e34c26", + CSS: "#1572b6", + Java: "#ed8b00", + "C++": "#f34b7d", + C: "#6b7280", + Ruby: "#cc342d", + PHP: "#4f5d95", + "C#": "#22c55e", // Better contrast green + Swift: "#fa7343", + Kotlin: "#7f52ff", + Lua: "#2c2d72", + "Vim Script": "#22c55e", // Better contrast green + "Emacs Lisp": "#863a9e", + }; + + return colors[language] || "#6b6b6b"; + }; + + if (loading) { + return ( +
+
+
+

Loading repositories...

+
+
+ ); + } + if (error) { + return ( +
+
+

Error loading repositories

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+ setSearchTerm(e.target.value)} + className={styles.searchInput} + aria-label="Search repositories, descriptions, or authors" + /> +
+
+ {" "} + +
+
+ + +
+
+ {/* Tag Filter Section */} +
+
+

Filter by Tags

+ {(selectedTags.length > 0 || + searchTerm || + selectedLanguage !== "all") && ( + + )} +
+
+ {(showAllTags ? tagStats : tagStats.slice(0, 12)).map( + ({ tag, count }) => ( + + ), + )} + {tagStats.length > 12 && ( + + )} +
+
+
+

+ Showing {filteredAndSortedRepos.length} of{" "} + {repos.length} repositories + {searchTerm && ` matching "${searchTerm}"`} + {selectedLanguage !== "all" && ` in ${selectedLanguage}`} + {selectedTags.length > 0 && ` with tags: ${selectedTags.join(", ")}`} +

+
{" "} +
+ {filteredAndSortedRepos.map((repo) => ( +
+ {viewMode === "expanded" ? ( + // Expanded card view (original design) + <> +
+
+ {`${repo.owner.login} +
+

+ + {repo.name} + +

+

by {repo.owner.login}

+
+
+
+ + + {repo.stargazers_count} + +
+
+ +

+ {repo.description || "No description available"} +

+ +
+
+ {/* language */} + {/* {repo.language && ( + + {repo.language} + + )} */} + + Updated {formatDate(repo.updated_at)} + +
{" "} + {repo.topics.filter( + (topic) => topic !== "talonvoice" && topic !== "talon", + ).length > 0 && ( +
+ {repo.topics + .filter( + (topic) => + topic !== "talonvoice" && topic !== "talon", + ) + .slice(0, 4) + .map((topic) => ( + + ))} + {repo.topics.filter( + (topic) => topic !== "talonvoice" && topic !== "talon", + ).length > 4 && ( + + + + {repo.topics.filter( + (topic) => + topic !== "talonvoice" && topic !== "talon", + ).length - 4}{" "} + more + + )} +
+ )} +
+ + ) : ( + // Compact card view + <> +
+
+ {`${repo.owner.login} +
+

+ + {repo.name} + +

+

+ by {repo.owner.login} +

+
+
+
+ + + {repo.stargazers_count} + +
+
+ +

+ {repo.description || "No description available"} +

+ +
+ {/* Language */} + {/* {repo.language && ( + + {repo.language} + + )} */} + + {formatDate(repo.updated_at)} + {" "} + {repo.topics.filter( + (topic) => topic !== "talonvoice" && topic !== "talon", + ).length > 0 && ( +
+ {repo.topics + .filter( + (topic) => + topic !== "talonvoice" && topic !== "talon", + ) + .slice(0, 2) + .map((topic) => ( + + ))} + {repo.topics.filter( + (topic) => topic !== "talonvoice" && topic !== "talon", + ).length > 2 && ( + + + + {repo.topics.filter( + (topic) => + topic !== "talonvoice" && topic !== "talon", + ).length - 2} + + )} +
+ )} +
+ + )} +
+ ))} +
+ {filteredAndSortedRepos.length === 0 && !loading && ( +
+

No repositories found

+

Try adjusting your search terms or filters.

+
+ )} +
+ ); +}; + +export default RepoExplorer; diff --git a/src/components/RepoExplorer/styles.module.css b/src/components/RepoExplorer/styles.module.css new file mode 100644 index 00000000..c2808505 --- /dev/null +++ b/src/components/RepoExplorer/styles.module.css @@ -0,0 +1,736 @@ +.container { + max-width: none; /* Allow full width */ + width: 100%; + margin: 0; + padding: 1rem 0; /* Only top/bottom padding */ + box-sizing: border-box; +} + +/* Loading and Error States */ +.loading, +.error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + text-align: center; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--ifm-color-emphasis-300); + border-top: 4px solid var(--ifm-color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.retryButton { + background: var(--ifm-color-primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + margin-top: 1rem; + font-size: 1rem; + transition: background-color 0.2s; +} + +.retryButton:hover { + background: var(--ifm-color-primary-dark); +} + +/* Controls */ +.controls { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; + align-items: center; +} + +.searchContainer { + flex: 1; + min-width: 250px; +} + +.searchInput { + width: 100%; + padding: 0.75rem; + border-radius: 6px; + font-size: 1rem; + border: 1px solid var(--ifm-color-emphasis-500); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + transition: border-color 0.2s; +} + +[data-theme="dark"] .searchInput { + border: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); +} + +.searchInput:focus { + outline: none; + border-color: var(--ifm-color-primary); +} + +.filterContainer, +.sortContainer, +.viewToggle { + min-width: 160px; +} + +.viewToggle { + display: flex; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + overflow: hidden; + min-width: auto; +} + +.viewButton { + padding: 0.75rem; + border: none; + background: var(--ifm-background-color); + color: var(--ifm-color-emphasis-600); + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.viewButton:hover { + background: var(--ifm-color-emphasis-100); +} + +.viewButtonActive { + background: var(--ifm-color-primary); + color: white; +} + +.viewButtonActive:hover { + background: var(--ifm-color-primary-dark); +} + +.languageFilter, +.sortSelect { + width: 100%; + padding: 0.75rem; + border-radius: 6px; + border: 1px solid var(--ifm-color-emphasis-500); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 1rem; + cursor: pointer; + appearance: none; + background-image: + linear-gradient(45deg, transparent 50%, var(--ifm-font-color-base) 50%), + linear-gradient(135deg, var(--ifm-font-color-base) 50%, transparent 50%); + background-position: + calc(100% - 20px) calc(1em + 2px), + calc(100% - 15px) calc(1em + 2px); + background-size: + 5px 5px, + 5px 5px; + background-repeat: no-repeat; + padding-right: 2.5rem; +} + +[data-theme="dark"] .languageFilter, +[data-theme="dark"] .sortSelect { + border: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); +} + +.languageFilter:focus, +.sortSelect:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 2px var(--ifm-color-primary-lightest); +} + +/* Stats */ +.stats { + margin-bottom: 1.5rem; + color: var(--ifm-color-emphasis-900); + font-size: 0.95rem; +} + +[data-theme="dark"] .stats { + color: var(--ifm-color-emphasis-600); +} + +/* Tag Filters */ +.tagFilters { + margin-bottom: 1.5rem; + padding: 1rem; + border: 1px solid var(--ifm-color-emphasis-500); + background: var(--ifm-background-surface-color); + border-radius: 8px; +} + +[data-theme="dark"] .tagFilters { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + background: var(--ifm-background-surface-color); +} + +.tagFiltersHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.tagFiltersTitle { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--ifm-color-emphasis-800); +} + +.tagGeneric { + background: none; + border: 1px solid var(--ifm-color-emphasis-300); + color: var(--ifm-color-emphasis-700); +} + +.tagGeneric:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-400); +} + +.tagGenericActive { + background: var(--ifm-color-primary-dark); + border-color: var(--ifm-color-primary); + color: white; +} + +[data-theme="dark"] .tagGenericActive { + background: var(--ifm-color-primary-darkest-alt); + border-color: var(--ifm-color-primary); + color: white; +} + +.tagGenericActive:hover { + background: var(--ifm-color-primary-light); + border-color: var(--ifm-color-primary); +} + +[data-theme="dark"] .tagGenericActive:hover { + background: var(--ifm-color-primary-darkest); + border-color: var(--ifm-color-primary); +} + +.clearFilters { + background: none; + border: 1px solid var(--ifm-color-emphasis-300); + color: var(--ifm-color-emphasis-700); + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; +} + +.clearFilters:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-400); +} + +.tagContainer { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.tagFilter { + padding: 0.375rem 0.75rem; + border-radius: 20px; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.moreTagsIndicator { + color: var(--ifm-color-emphasis-600); + font-size: 0.875rem; + font-style: italic; + margin-left: 0.5rem; +} + +.expandTagsButton { + background: none; + border: 1px solid var(--ifm-color-emphasis-400); + color: var(--ifm-color-emphasis-700); + padding: 0.375rem 0.75rem; + border-radius: 20px; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + margin-left: 0.5rem; +} + +.expandTagsButton:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-500); +} + +/* Grid Layout */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.gridCompact { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; +} + +/* Repository Cards */ +.card { + border: 1px solid var(--ifm-color-emphasis-500); + border-radius: 12px; + padding: 1.5rem; + background: var(--ifm-background-color); + transition: all 0.2s ease; + height: fit-content; +} + +[data-theme="dark"] .card { + border-color: var(--ifm-color-emphasis-300); + background: var(--ifm-background-surface-color); +} + +[data-theme="dark"] .card:hover { + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); +} + +.cardCompact { + padding: 1rem; + border-radius: 8px; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + border-color: var(--ifm-color-primary-light); +} + +/* Compact Card Components */ +.cardHeaderCompact { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + gap: 0.75rem; +} + +.repoInfoCompact { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 0; +} + +.avatarCompact { + width: 28px; + height: 28px; + border-radius: 50%; + flex-shrink: 0; + border: 1px solid var(--ifm-color-emphasis-200); +} + +.repoDetailsCompact { + min-width: 0; + flex: 1; +} + +.repoNameCompact { + margin: 0; + font-size: 1rem; + font-weight: 600; + line-height: 1.2; +} + +.descriptionCompact { + color: var(--ifm-color-emphasis-800); + line-height: 1.4; + margin-bottom: 0.75rem; + font-size: 0.875rem; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.metadataCompact { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + flex-wrap: wrap; +} + +.languageCompact { + padding: 0.2rem 0.5rem; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + flex-shrink: 0; + color: #ffffff !important; /* Force white text for all backgrounds */ + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.topicsCompact { + display: flex; + gap: 0.25rem; + margin-left: auto; + flex-shrink: 0; +} + +.topicCompact { + padding: 0.15rem 0.4rem; + border-radius: 8px; + font-size: 0.75rem; + font-weight: 500; + flex-shrink: 0; + cursor: pointer; + transition: all 0.2s; +} + +.topicMoreCompact { + padding: 0.15rem 0.4rem; + background: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-700); + border-radius: 8px; + font-size: 0.75rem; + font-weight: 500; + flex-shrink: 0; +} + +[data-theme="dark"] .topicMoreCompact { + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-600); +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + gap: 1rem; +} + +.repoInfo { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; + min-width: 0; /* Allow flex child to shrink */ +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; + border: 2px solid var(--ifm-color-emphasis-200); +} + +.repoDetails { + min-width: 0; /* Allow flex child to shrink */ + flex: 1; +} + +.repoName { + margin: 0 0 0.25rem 0; + font-size: 1.2rem; + font-weight: 600; + line-height: 1.3; +} + +.repoLink { + color: var(--ifm-color-primary); + text-decoration: none; + word-break: break-word; +} + +.repoLink:hover { + text-decoration: underline; +} + +.repoOwner { + margin: 0; + font-size: 0.875rem; + color: var(--ifm-color-emphasis-900); + word-break: break-word; +} + +.repoOwnerCompact { + margin: 0; + font-size: 0.75rem; + color: var(--ifm-color-emphasis-900); + word-break: break-word; +} + +[data-theme="dark"] .repoOwner, +[data-theme="dark"] .repoOwnerCompact { + color: var(--ifm-color-emphasis-600); +} + +.stars { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + flex-shrink: 0; + color: var(--ifm-color-emphasis-700); + background: var(--ifm-color-emphasis-100); + padding: 0.25rem 0.5rem; + border-radius: 12px; +} + +.starsCompact { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + flex-shrink: 0; + color: var(--ifm-color-emphasis-700); + background: var(--ifm-color-emphasis-100); + padding: 0.2rem 0.4rem; + border-radius: 8px; +} + +[data-theme="dark"] .stars, +[data-theme="dark"] .starsCompact { + color: var(--ifm-color-emphasis-700); + background: var(--ifm-color-emphasis-100); +} + +.starIcon { + font-size: 1rem; +} + +.starCount { + font-weight: 500; +} + +.description { + color: var(--ifm-color-emphasis-800); + line-height: 1.5; + margin-bottom: 1.25rem; + min-height: 3rem; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 0.95rem; +} + +.cardFooter { + border-top: 1px solid var(--ifm-color-emphasis-200); + padding-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.metadata { + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.875rem; + flex-wrap: wrap; +} + +.language { + padding: 0.25rem 0.75rem; + border-radius: 16px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + color: #ffffff !important; /* Force white text for all backgrounds */ + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.updated { + color: var(--ifm-color-emphasis-900); + flex-shrink: 0; +} + +.updatedCompact { + color: var(--ifm-color-emphasis-900); + flex-shrink: 0; +} + +[data-theme="dark"] .updated { + color: var(--ifm-color-emphasis-600); +} + +[data-theme="dark"] .updatedCompact { + color: var(--ifm-color-emphasis-600); +} + +.topics { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.topic { + padding: 0.25rem 0.75rem; + border-radius: 16px; + font-size: 0.75rem; + font-weight: 500; + flex-shrink: 0; + cursor: pointer; + transition: all 0.2s; +} + +.topic:hover { + transform: translateY(-1px); +} + +.topicMore { + padding: 0.25rem 0.75rem; + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-600); + border-radius: 16px; + font-size: 0.75rem; + font-weight: 500; + flex-shrink: 0; +} + +/* No Results */ +.noResults { + text-align: center; + padding: 4rem 1rem; + color: var(--ifm-color-emphasis-600); +} + +.noResults h3 { + margin-bottom: 0.5rem; + color: var(--ifm-color-emphasis-700); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .gridCompact { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + .searchContainer, + .filterContainer, + .sortContainer, + .viewToggle { + min-width: unset; + } + + .cardHeader { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .stars { + align-self: flex-start; + } + + .metadata { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .container { + padding: 0 0.5rem; + } +} + +@media (max-width: 480px) { + .card { + padding: 1rem; + } + + .cardCompact { + padding: 0.75rem; + } + + .repoInfo { + gap: 0.5rem; + } + + .avatar { + width: 32px; + height: 32px; + } + + .avatarCompact { + width: 24px; + height: 24px; + } + + .repoName { + font-size: 1.1rem; + } + + .repoNameCompact { + font-size: 0.95rem; + } +} + +.warning { + font-size: "1rem"; + color: "white"; + line-height: 1.5; + margin: 0; + margin-top: 1rem; + text-align: left; + background: var(--ifm-color-warning-contrast-background); + border: 1px solid var(--ifm-color-warning-light); + border-radius: 6px; + padding: 0.75rem; + border-left-width: 3px; + border-left-color: var(--ifm-color-warning); +} diff --git a/src/css/custom.css b/src/css/custom.css index bfdc8a4d..bcf61f72 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -10,6 +10,7 @@ --ifm-color-primary-dark: #29784c; --ifm-color-primary-darker: #277148; --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-darkest-alt: #1a4b2f; --ifm-color-primary-light: #33925d; --ifm-color-primary-lighter: #359962; --ifm-color-primary-lightest: #3cad6e; @@ -26,6 +27,7 @@ /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme="dark"] { --ifm-color-primary: #25c2a0; + --ifm-color-primary-alt: #279586; --ifm-color-primary-dark: #21af90; --ifm-color-primary-darker: #1fa588; --ifm-color-primary-darkest: #1a8870; diff --git a/src/pages/explorer.module.css b/src/pages/explorer.module.css new file mode 100644 index 00000000..54cd0031 --- /dev/null +++ b/src/pages/explorer.module.css @@ -0,0 +1,73 @@ +.warning { + font-size: "1rem"; + color: "white"; + line-height: 1.5; + margin: 0; + margin-top: 1rem; + text-align: left; + background: var(--ifm-color-warning-contrast-background); + border: 1px solid var(--ifm-color-warning-light); + border-radius: 6px; + padding: 0.75rem; + border-left-width: 3px; + border-left-color: var(--ifm-color-warning); +} + +.warning a { + color: var(--ifm-color-emphasis-700); + text-decoration: underline; + font-weight: bold; +} + +[data-theme="dark"] .warning a { + color: var(--ifm-color-warning); +} + +.pageTitle { + font-size: 3rem; + margin-bottom: 1rem; + background: none; + color: #000000; + background-clip: text; + text-align: left; +} + +[data-theme="dark"] .pageTitle { + background: linear-gradient( + 45deg, + var(--ifm-color-primary-dark), + var(--ifm-color-secondary-dark) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-align: left; +} + +.pageDescription { + font-size: 1.25rem; + color: var(--ifm-color-emphasis-700); + line-height: 1.6; + margin: 0 0 0.75rem 0; + text-align: left; +} + +.codeHighlight { + background: var(--ifm-color-emphasis-200); + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-weight: bold; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .pageTitle { + text-align: center !important; + font-size: 2.25rem !important; + } + + .pageDescription { + text-align: center !important; + font-size: 1rem !important; + } +} diff --git a/src/pages/explorer.tsx b/src/pages/explorer.tsx new file mode 100644 index 00000000..edb4fa29 --- /dev/null +++ b/src/pages/explorer.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Layout from "@theme/Layout"; +import RepoExplorer from "../components/RepoExplorer"; +import styles from "./explorer.module.css"; + +export default function Explorer(): JSX.Element { + return ( + +
+
+
+ {/* Title and Description */} +
+

Repository Explorer

+

+ Discover Talon-related repositories from GitHub tagged with{" "} + talonvoice +

+

+ ⚠️ Use at your own risk: These repositories may + not be curated or tested. For curated packages, visit{" "} + + talon_user_file_sets + + . +

+
+
+ + +
+
+
+ ); +}