3ee9b9e6de
CI / build (push) Has been cancelled
- Introduce PolicyModal component for license and privacy policy display - Add Quick Start section to project detail pages - Update project descriptions, features, and URLs for several projects - Add new logos, screenshots, and favicon - Extend types and configuration for new features
232 lines
8.4 KiB
TypeScript
232 lines
8.4 KiB
TypeScript
import { Link, useParams } from 'react-router-dom';
|
|
import { useI18n } from '../hooks/useI18n';
|
|
import { siteData } from '../data/siteData';
|
|
import DownloadTable from '../components/DownloadTable';
|
|
import RoadmapGrid from '../components/RoadmapGrid';
|
|
import ChangelogList from '../components/ChangelogList';
|
|
import ScreenshotCarousel from '../components/ScreenshotCarousel';
|
|
import { getIcon } from '../utils/iconRegistry';
|
|
import { ExternalLink, Download, BookOpen, Globe } from 'lucide-react';
|
|
|
|
export default function ProjectDetailPage() {
|
|
const { id } = useParams();
|
|
const { t, bi, biArray, lang } = useI18n();
|
|
const p = siteData.projects.find((pr) => pr.id === id);
|
|
|
|
if (!p) {
|
|
return (
|
|
<div className="container">
|
|
<div className="empty-state">Project not found</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const statusDef = siteData.statuses[p.status];
|
|
const hasDownloads = p.downloads && p.downloads.length > 0;
|
|
const IconComponent = getIcon(p.icon);
|
|
|
|
return (
|
|
<div className="container fade-in">
|
|
{/* Breadcrumb */}
|
|
<div className="breadcrumb">
|
|
<Link to="/">
|
|
{bi(siteData.nav[0].label)}
|
|
</Link>
|
|
{' / '}
|
|
<Link to="/projects">
|
|
{bi(siteData.nav[1].label)}
|
|
</Link>
|
|
{' / '}
|
|
<span className="breadcrumb-current">{p.name}</span>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="detail-header">
|
|
<div className="detail-header-top">
|
|
<div className="detail-icon">
|
|
{p.logo
|
|
? <img src={p.logo} alt={p.name} className="project-logo" />
|
|
: (IconComponent ? <IconComponent size={28} /> : <span>{p.icon}</span>)
|
|
}
|
|
</div>
|
|
<div>
|
|
<h1 className="detail-title">{p.displayName[lang] || p.name}</h1>
|
|
<p className="detail-slogan">{bi(p.slogan)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="detail-badges">
|
|
{p.techStack.map((ts) => (
|
|
<span key={ts} className="badge badge-accent">
|
|
{ts}
|
|
</span>
|
|
))}
|
|
{p.platforms.map((pl) => (
|
|
<span key={pl} className="badge">
|
|
{pl}
|
|
</span>
|
|
))}
|
|
<span
|
|
className="badge badge-status"
|
|
style={{ background: statusDef?.color || '#6B6B6B' }}
|
|
>
|
|
{bi(statusDef?.label)}
|
|
</span>
|
|
</div>
|
|
<div className="detail-actions">
|
|
{p.websiteUrl && (
|
|
<a href={p.websiteUrl} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
|
<Globe size={16} />
|
|
{t('detail.website')}
|
|
</a>
|
|
)}
|
|
{hasDownloads && (
|
|
<a href="#downloads" className="btn" onClick={(e) => {
|
|
e.preventDefault();
|
|
document.getElementById('downloads')?.scrollIntoView({ behavior: 'smooth' });
|
|
}}>
|
|
<Download size={16} />
|
|
{t('common.download')}
|
|
</a>
|
|
)}
|
|
{p.docsUrl && (
|
|
<a href={p.docsUrl} target="_blank" rel="noopener noreferrer" className="btn">
|
|
<BookOpen size={16} />
|
|
{t('common.docs')}
|
|
</a>
|
|
)}
|
|
<a href={`${p.repoUrl}/issues`} target="_blank" className="btn btn-ghost">
|
|
<ExternalLink size={16} />
|
|
{t('contact.issues')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="detail-body">
|
|
<div className="detail-main">
|
|
{/* Overview */}
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.overview')}</h2>
|
|
<p className="detail-prose">{bi(p.description)}</p>
|
|
</div>
|
|
|
|
{/* Quick Start */}
|
|
{p.quickStart && p.quickStart[lang] && p.quickStart[lang].length > 0 && (
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.quickStart')}</h2>
|
|
<div className="quickstart-commands">
|
|
{p.quickStart[lang].map((cmd, i) => (
|
|
<div key={i} className="quickstart-line">
|
|
<span className="quickstart-prompt">$</span>
|
|
<code className="quickstart-cmd">{cmd}</code>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Features */}
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.features')}</h2>
|
|
<div className="feature-tags">
|
|
{biArray(p.features).map((f, i) => (
|
|
<span key={i} className="feature-tag">
|
|
{f}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Screenshots */}
|
|
{p.screenshots && p.screenshots.length > 0 && (
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.screenshots')}</h2>
|
|
<ScreenshotCarousel screenshots={p.screenshots} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Downloads */}
|
|
{hasDownloads && (
|
|
<div className="detail-section" id="downloads">
|
|
<h2 className="detail-section-title">{t('detail.downloads')}</h2>
|
|
<DownloadTable downloads={p.downloads} />
|
|
{p.installGuide && (
|
|
<div className="install-guide-inline">
|
|
{p.installGuide[lang].map((item) => (
|
|
<div key={item.platform} className="install-tip-row">
|
|
<span className="install-tip-icon">{item.icon}</span>
|
|
<span className="install-tip-format">{item.format}</span>
|
|
<span className="install-tip-text">{item.tip}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
<div className="trust-note">{t('downloads.trustNote')}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Changelog */}
|
|
{p.changelog && p.changelog.length > 0 && (
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.changelog')}</h2>
|
|
<ChangelogList changelog={p.changelog} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Roadmap */}
|
|
{p.roadmap && (
|
|
<div className="detail-section">
|
|
<h2 className="detail-section-title">{t('detail.roadmap')}</h2>
|
|
<RoadmapGrid roadmap={p.roadmap} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<aside className="detail-sidebar">
|
|
<div className="detail-meta-panel">
|
|
<div className="detail-meta-item">
|
|
<span className="detail-meta-label">{t('detail.version')}</span>
|
|
<span className="detail-meta-value mono">{p.latestVersion}</span>
|
|
</div>
|
|
<div className="detail-meta-item">
|
|
<span className="detail-meta-label">{t('detail.status')}</span>
|
|
<span className="detail-meta-value">{bi(statusDef?.label)}</span>
|
|
</div>
|
|
<div className="detail-meta-item">
|
|
<span className="detail-meta-label">{t('detail.license')}</span>
|
|
<span className="detail-meta-value">{p.license}</span>
|
|
</div>
|
|
<div className="detail-meta-item">
|
|
<span className="detail-meta-label">{t('detail.lastUpdate')}</span>
|
|
<span className="detail-meta-value">{p.lastUpdated}</span>
|
|
</div>
|
|
<div className="detail-meta-item">
|
|
<span className="detail-meta-label">Language</span>
|
|
<span className="detail-meta-value">{p.language}</span>
|
|
</div>
|
|
</div>
|
|
<div className="detail-link-grid">
|
|
<a href={p.repoUrl} target="_blank" className="detail-link-btn">
|
|
<ExternalLink size={15} />
|
|
{t('detail.repo')}
|
|
</a>
|
|
{p.websiteUrl && (
|
|
<a href={p.websiteUrl} target="_blank" rel="noopener noreferrer" className="detail-link-btn">
|
|
<Globe size={15} />
|
|
{t('detail.website')}
|
|
</a>
|
|
)}
|
|
{p.docsUrl && (
|
|
<a href={p.docsUrl} target="_blank" rel="noopener noreferrer" className="detail-link-btn">
|
|
<BookOpen size={15} />
|
|
{t('detail.docs')}
|
|
</a>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|