diff --git a/.gitignore b/.gitignore index 6e7cd7a..ffb9e09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Dependencies node_modules +pnpm-lock.yaml +yarn.lock # Build outputs dist diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4090f28..b5b923a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,30 @@ +# Contributing to FolioLab + +Thank you for your interest in contributing! + +## Getting Started + +1. Install dependencies: +```bash npm install ``` 2. Set up environment variables: -Create a `.env` file with the required GitHub OAuth credentials. +Create a `.env` file with the required GitHub OAuth credentials (see `.env.example`). 3. Start the development server: ```bash -npm run dev \ No newline at end of file +npm run dev +``` + +## Running Tests + +```bash +npm run test:run +``` + +## Building + +```bash +npm run build +``` diff --git a/client/src/App.tsx b/client/src/App.tsx index fec3f4a..75acb34 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,3 +1,4 @@ +import DataSourcesPage from "@/pages/data-sources"; import { Switch, Route } from "wouter"; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; @@ -24,6 +25,7 @@ function Router() { + diff --git a/client/src/pages/data-sources.tsx b/client/src/pages/data-sources.tsx index 6ff819c..8aaec15 100644 --- a/client/src/pages/data-sources.tsx +++ b/client/src/pages/data-sources.tsx @@ -278,6 +278,7 @@ export default function DataSourcesPage() { @@ -327,6 +328,7 @@ export default function DataSourcesPage() { @@ -375,6 +377,7 @@ export default function DataSourcesPage() { @@ -414,6 +417,7 @@ export default function DataSourcesPage() { @@ -465,6 +469,7 @@ export default function DataSourcesPage() { @@ -516,6 +521,7 @@ export default function DataSourcesPage() { @@ -614,6 +620,7 @@ export default function DataSourcesPage() { diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 761e118..1eb400c 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -1,6 +1,12 @@ import { Link } from "wouter"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { Github, Shield, Globe, FileQuestion, Mail, Info, Database } from "lucide-react"; import { clearAllData } from "@/lib/queryClient"; import { clearStorage } from "@/lib/storage"; @@ -103,34 +109,49 @@ export default function Home() {

Frequently Asked Questions

-
-

How does it work?

-

- Import content from GitHub, GitLab, Bitbucket, Medium, your blog, or add custom content. - Select what you want to showcase, and we'll generate beautiful summaries using AI. - Deploy your portfolio with one click to GitHub Pages or Vercel. -

-
-
-

What sources can I use?

-

- You can import from GitHub (OAuth), GitLab (personal access token), Bitbucket (app password), - Medium (username), any blog RSS feed, or create custom free-form content. Mix and match - as many sources as you like! -

-
-
-

Is it free to use?

-

- FolioLab is open-source and free to use. -

-
-
-

What data do you collect?

-

- We don't store any of your data. -

-
+ + + + How does it work? + + + Import content from GitHub, GitLab, Bitbucket, Medium, your + blog, or add custom content. Select what you want to showcase, + and we'll generate beautiful summaries using AI. Deploy your + portfolio with one click to GitHub Pages or Vercel. + + + + + + What sources can I use? + + + You can import from GitHub (OAuth), GitLab (personal access + token), Bitbucket (app password), Medium (username), any blog + RSS feed, or create custom free-form content. Mix and match as + many sources as you like! + + + + + + Is it free to use? + + + FolioLab is open-source and free to use. + + + + + + What data do you collect? + + + We don't store any of your data. + + +
diff --git a/client/src/pages/portfolio-preview.tsx b/client/src/pages/portfolio-preview.tsx index 328a271..845b0fc 100644 --- a/client/src/pages/portfolio-preview.tsx +++ b/client/src/pages/portfolio-preview.tsx @@ -525,6 +525,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={() => startEditingItemTitle(item.id, getItemTitle(item))} + aria-label="Edit item title" > @@ -539,7 +540,7 @@ export default function PortfolioPreview() { )} {item.url && ( - @@ -603,6 +605,7 @@ export default function PortfolioPreview() { onClick={() => deleteItem(item.id)} className="text-red-500 hover:text-red-700" title="Remove from portfolio" + aria-label="Remove from portfolio" > @@ -761,6 +764,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={startEditingTitle} + aria-label="Edit portfolio title" > @@ -806,6 +810,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={startEditingIntro} + aria-label="Edit introduction" > @@ -882,6 +887,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={startEditingSkills} + aria-label="Edit skills" > @@ -941,6 +947,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={startEditingInterests} + aria-label="Edit interests" > @@ -1056,6 +1063,7 @@ export default function PortfolioPreview() { variant="ghost" className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity" onClick={() => startEditingItemTitle(item.id, getItemTitle(item))} + aria-label="Edit item title" > @@ -1070,7 +1078,7 @@ export default function PortfolioPreview() { )} {item.url && ( - @@ -1134,6 +1143,7 @@ export default function PortfolioPreview() { onClick={() => deleteItem(item.id)} className="text-red-500 hover:text-red-700" title="Remove from portfolio" + aria-label="Remove from portfolio" > diff --git a/client/src/pages/repo-select.tsx b/client/src/pages/repo-select.tsx index 6c721d4..9f50d2a 100644 --- a/client/src/pages/repo-select.tsx +++ b/client/src/pages/repo-select.tsx @@ -8,7 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Input } from "@/components/ui/input"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { Repository } from "@shared/schema"; -import { Search, ExternalLink } from "lucide-react"; +import { Search, ExternalLink, X } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { toggleRepositorySelection, saveRepositories, getRepositories, getGitHubToken } from "@/lib/storage"; import { AnalysisProgress } from "@/components/analysis-progress"; @@ -273,8 +273,21 @@ export default function RepoSelect() { setSearchQuery(e.target.value); setCurrentPage(1); }} - className="pl-9" + className="pl-9 pr-9" /> + {searchQuery && ( + + )}
@@ -471,8 +484,9 @@ export default function RepoSelect() { onClick={() => repo.id && toggleRepo({ id: repo.id, selected: false })} className="ml-2 h-6 w-6 p-0 text-muted-foreground hover:text-destructive" title="Remove from selection" + aria-label={`Remove ${repo.displayName || repo.name} from selection`} > - × +
diff --git a/client/src/pages/select-items.tsx b/client/src/pages/select-items.tsx index 16bc8cb..5d53f03 100644 --- a/client/src/pages/select-items.tsx +++ b/client/src/pages/select-items.tsx @@ -1,9 +1,19 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useLocation } from 'wouter'; import { PortfolioItem, SourceType } from '@shared/schema'; import { getPortfolioItems, togglePortfolioItemSelection, savePortfolioItems } from '../lib/storage'; import { clearWizardState } from '../lib/wizard-state'; +const sourceLabels: Record = { + github: { label: 'GitHub', icon: '🐙', color: 'bg-gray-100 text-gray-800' }, + blog_rss: { label: 'Blog Posts', icon: '📰', color: 'bg-orange-100 text-orange-800' }, + medium: { label: 'Medium', icon: '📝', color: 'bg-green-100 text-green-800' }, + gitlab: { label: 'GitLab', icon: '🦊', color: 'bg-purple-100 text-purple-800' }, + bitbucket: { label: 'Bitbucket', icon: '🪣', color: 'bg-blue-100 text-blue-800' }, + linkedin: { label: 'LinkedIn', icon: '🔗', color: 'bg-cyan-100 text-cyan-800' }, + freeform: { label: 'Custom', icon: '✍️', color: 'bg-pink-100 text-pink-800' } +}; + // Helper functions to safely access properties across different item types const getItemTitle = (item: PortfolioItem): string => { if (item.source === 'github' || item.source === 'gitlab' || item.source === 'bitbucket') { @@ -82,33 +92,31 @@ export default function SelectItemsPage() { setLocation('/preview'); }; - const filteredItems = filter === 'all' + const filteredItems = useMemo(() => filter === 'all' ? items - : items.filter(item => item.source === filter); + : items.filter(item => item.source === filter), [items, filter]); - const groupedItems = filteredItems.reduce((acc, item) => { + const groupedItems = useMemo(() => filteredItems.reduce((acc, item) => { const source = item.source; if (!acc[source]) { acc[source] = []; } acc[source].push(item); return acc; - }, {} as Record); + }, {} as Record), [filteredItems]); const selectedCount = items.filter(item => item.selected).length; const totalCount = items.length; - const sourceLabels: Record = { - github: { label: 'GitHub', icon: '🐙', color: 'bg-gray-100 text-gray-800' }, - blog_rss: { label: 'Blog Posts', icon: '📰', color: 'bg-orange-100 text-orange-800' }, - medium: { label: 'Medium', icon: '📝', color: 'bg-green-100 text-green-800' }, - gitlab: { label: 'GitLab', icon: '🦊', color: 'bg-purple-100 text-purple-800' }, - bitbucket: { label: 'Bitbucket', icon: '🪣', color: 'bg-blue-100 text-blue-800' }, - linkedin: { label: 'LinkedIn', icon: '💼', color: 'bg-blue-100 text-blue-800' }, - freeform: { label: 'Custom', icon: '✍️', color: 'bg-pink-100 text-pink-800' } - }; + const uniqueSources = useMemo(() => Array.from(new Set(items.map(item => item.source))), [items]); - const uniqueSources = Array.from(new Set(items.map(item => item.source))); + const sourceCounts = useMemo(() => { + const counts: Record = {}; + items.forEach(item => { + counts[item.source] = (counts[item.source] || 0) + 1; + }); + return counts; + }, [items]); if (totalCount === 0) { return ( @@ -209,7 +217,7 @@ export default function SelectItemsPage() { All ({totalCount}) {uniqueSources.map(source => { - const count = items.filter(item => item.source === source).length; + const count = sourceCounts[source] || 0; const sourceInfo = sourceLabels[source]; return ( @@ -109,29 +110,29 @@ export default function SourceSelectionPage() { {/* Progress Indicator */}
-
-
+
+
1
Select Sources
-
-
+ +
2
Import Data
-
-
+ +
3
Select Items
-
-
+ +
4
@@ -152,12 +153,13 @@ export default function SourceSelectionPage() { key={source.type} onClick={() => !isDisabled && toggleSource(source.type)} disabled={isDisabled} - className={`${source.color} border-2 rounded-lg p-6 text-left hover:shadow-lg transition-all relative ${ + aria-pressed={isSelected} + className={`${source.color} border-2 rounded-lg p-6 text-left hover:shadow-lg transition-all relative focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 ${ isSelected ? 'ring-4 ring-blue-500 ring-opacity-50' : '' } ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}`} > {/* Selection Checkbox */} -
+