Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ A modern, responsive web application for discovering and exploring modules and d

<img width="1277" height="773" alt="Знімок екрана 2025-08-06 о 17 01 00" src="https://github.com/user-attachments/assets/8be09316-6d87-47ca-8b70-15619d3d5f1f" />


## 🚀 Features

- **Dual Interface**: Browse both modules and data sources with dedicated views
Expand Down Expand Up @@ -69,6 +68,7 @@ A modern, responsive web application for discovering and exploring modules and d
The application fetches data from multiple sources:

### Modules Data

- **ASIMOV Platform API**: Module metadata, stars, and GitHub information
- **GitHub API**: Live repository data and statistics
- **YAML Manifests**: Module configuration and metadata
Expand All @@ -91,6 +91,7 @@ The project uses a custom color palette defined in `src/styles/global.css`:
### Navigation Structure

Two main sections:

- **Sources** (`/`): Browse data sources with endpoints and formats
- **Modules** (`/modules`): Explore GitHub modules with metadata

Expand Down Expand Up @@ -165,6 +166,7 @@ npm run format:check # Check code formatting
## 📊 Performance & Caching

### Query Caching

- **TanStack Query**: Intelligent data caching with stale-while-revalidate
- **Optimistic Updates**: Smooth user experience with background refetching
- **Error Recovery**: Automatic retry logic for failed requests
Expand Down
776 changes: 401 additions & 375 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"devDependencies": {
"@astrojs/sitemap": "^3.2.1",
"@eslint/compat": "^1.2.3",
"@types/js-yaml": "^4.0.9",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.32.0",
Expand All @@ -41,6 +40,8 @@
"astro": "^5.12.8",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwindcss": "^4.1.11"
"react-infinite-scroll-component": "^6.1.0",
"tailwindcss": "^4.1.11",
"yaml": "^2.8.1"
}
}
111 changes: 43 additions & 68 deletions src/components/GitHubStats.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,60 @@
import { useQuery } from '@tanstack/react-query';
import { Star, Users } from '@phosphor-icons/react';
import { githubApi, formatStars } from '../lib/github';
import { fetchTotalModuleStars, fetchOrgFollowers } from '../lib/github';
import { asimovModulesOrgUrl, asimovPlatformOrgUrl } from '../lib/config';
import { formatStars } from '../lib/utils';
import { queryClient } from '../store';

import MetricsBadge from './MetricsBadge';

interface GitHubStatsProps {
variant?: 'desktop' | 'mobile';
}

export default function GitHubStats({ variant = 'desktop' }: GitHubStatsProps) {
const githubStatsQuery = useQuery({
queryKey: ['github-stats'],
queryFn: () => githubApi.fetchGitHubStats(),
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 20 * 60 * 1000, // 20 minutes
retry: 1
}, queryClient);
const { data: stars, isLoading: isStarsLoading } = useQuery(
{
queryKey: ['asimov-modules-stars'],
queryFn: fetchTotalModuleStars,
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 20 * 60 * 1000, // 20 minutes
retry: 2
},
queryClient
);

const { data: followers, isLoading: isFollowersLoading } = useQuery(
{
queryKey: ['asimov-platform-followers'],
queryFn: fetchOrgFollowers,
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 20 * 60 * 1000, // 20 minutes
retry: 2
},
queryClient
);

const containerClass =
variant === 'desktop'
? 'flex items-center gap-3'
: 'flex items-center justify-center gap-4 py-2';

const linkClass =
variant === 'desktop'
? 'group flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white/80 px-3 py-1.5 text-sm transition-all hover:border-orange-200 hover:bg-orange-50'
: 'flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm';

const iconClass =
variant === 'desktop' ? 'text-gray-500 group-hover:text-orange-600' : 'text-gray-500';

const textClass =
variant === 'desktop'
? 'font-medium text-gray-700 group-hover:text-orange-700'
: 'font-medium text-gray-700';

const loadingContainerClass =
variant === 'desktop'
? 'flex items-center gap-3'
: 'flex items-center justify-center gap-4 py-2';

if (githubStatsQuery.data) {
return (
<div className={containerClass}>
<a
href={asimovModulesOrgUrl}
target="_blank"
rel="noopener noreferrer"
className={linkClass}
>
<Star size={14} className={iconClass} />
<span className={textClass}>
{formatStars(githubStatsQuery.data.stars)}
</span>
</a>
<a
href={asimovPlatformOrgUrl}
target="_blank"
rel="noopener noreferrer"
className={linkClass}
>
<Users size={14} className={iconClass} />
<span className={textClass}>
{formatStars(githubStatsQuery.data.followers)}
</span>
</a>
</div>
);
}

if (githubStatsQuery.isLoading) {
return (
<div className={loadingContainerClass}>
<div className="animate-pulse rounded-lg bg-gray-200 px-3 py-1.5">
<div className="h-4 w-8 rounded bg-gray-300"></div>
</div>
<div className="animate-pulse rounded-lg bg-gray-200 px-3 py-1.5">
<div className="h-4 w-8 rounded bg-gray-300"></div>
</div>
</div>
);
}
return null;
return (
<div className={containerClass}>
<MetricsBadge
value={formatStars(stars ?? 0)}
href={asimovModulesOrgUrl}
icon={Star}
variant={variant}
loading={isStarsLoading}
/>
<MetricsBadge
value={formatStars(followers ?? 0)}
href={asimovPlatformOrgUrl}
icon={Users}
variant={variant}
loading={isFollowersLoading}
/>
</div>
);
}
43 changes: 43 additions & 0 deletions src/components/MetricsBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface MetricsBadgeProps {
href: string;
value: string | number;
icon: React.ComponentType<{ size: number; className?: string }>;
variant?: 'desktop' | 'mobile';
loading?: boolean;
}

export default function MetricsBadge({
href,
value,
icon: Icon,
variant,
loading
}: MetricsBadgeProps) {
const linkClass =
variant === 'desktop'
? 'group flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white/80 px-3 py-1.5 text-sm transition-all hover:border-orange-200 hover:bg-orange-50'
: 'flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm';

const iconClass =
variant === 'desktop' ? 'text-gray-500 group-hover:text-orange-600' : 'text-gray-500';

const textClass =
variant === 'desktop'
? 'font-medium text-gray-700 group-hover:text-orange-700'
: 'font-medium text-gray-700';

return (
<>
{loading ? (
<div className="animate-pulse rounded-lg bg-gray-200 px-3 py-1.5">
<div className="h-4 w-8 rounded bg-gray-300"></div>
</div>
) : (
<a href={href} target="_blank" rel="noopener noreferrer" className={linkClass}>
<Icon size={14} className={iconClass} />
<span className={textClass}>{value}</span>
</a>
)}
</>
);
}
Loading