Compare commits

...

4 commits

Author SHA1 Message Date
CDN
3e652d5e4e
chore: add analytics
All checks were successful
Deploy / Deploy (push) Successful in 1m4s
2025-02-03 16:41:13 +08:00
CDN
7de33c3931
fix: search bar too close to title on updates page in mobile view 2025-02-03 16:39:45 +08:00
CDN
4ba16a7ddd
feat: rss feed
closes #2
2025-02-03 16:31:04 +08:00
CDN
e30d41db2f
chore: update links in README 2025-02-03 15:58:40 +08:00
8 changed files with 165 additions and 19 deletions

View file

@ -46,7 +46,7 @@ homepage
## Contribution Guide
See [CONTRIBUTING.md](docs/zh-CN/CONTRIBUTING.md) in the `docs` directory to learn how to participate in the project.
See [CONTRIBUTING.md](docs/en-US/CONTRIBUTING.md) in the `docs` directory to learn how to participate in the project.
## License

View file

@ -46,7 +46,7 @@ homepage
## 貢獻指南
參見 `docs` 目錄中的 [CONTRIBUTING.md](docs/zh-CN/CONTRIBUTING.md) 了解如何參與專案。
參見 `docs` 目錄中的 [CONTRIBUTING.md](docs/zh-TW/CONTRIBUTING.md) 了解如何參與專案。
## 許可

View file

@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>STARSET Mirror</title>
<script defer src="https://analytics.owu.one/script.js" data-website-id="1f9f0242-8bce-4883-be1d-4bb7a3aad6d0"></script>
</head>
<body>
<div id="root"></div>

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vite build && tsx scripts/generate-rss.ts",
"lint": "eslint .",
"preview": "vite preview"
},
@ -14,6 +14,7 @@
"@tanstack/react-query": "^5.66.0",
"antd": "^5.23.3",
"clsx": "^2.1.1",
"feed": "^4.2.2",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^7.2.2",
"lucide-react": "^0.344.0",

24
pnpm-lock.yaml generated
View file

@ -20,6 +20,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
feed:
specifier: ^4.2.2
version: 4.2.2
i18next:
specifier: ^23.16.8
version: 23.16.8
@ -1138,6 +1141,10 @@ packages:
fastq@1.18.0:
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
feed@4.2.2:
resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
engines: {node: '>=0.4.0'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -1845,6 +1852,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@ -2039,6 +2049,10 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -3103,6 +3117,10 @@ snapshots:
dependencies:
reusify: 1.0.4
feed@4.2.2:
dependencies:
xml-js: 1.6.11
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -3857,6 +3875,8 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
sax@1.4.1: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@ -4038,6 +4058,10 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
xml-js@1.6.11:
dependencies:
sax: 1.4.1
yallist@3.1.1: {}
yaml@2.7.0: {}

103
scripts/generate-rss.ts Normal file
View file

@ -0,0 +1,103 @@
import { Feed } from 'feed';
import fs from 'fs-extra';
import path from 'path';
import { Update } from '../src/utils/updates';
const SITE_URL = 'https://starset.wiki';
interface LanguageConfig {
code: string;
dataDir: string;
title: string;
description: string;
}
const LANGUAGES: LanguageConfig[] = [
{
code: 'en-US',
dataDir: 'en-US',
title: 'STARSET Mirror Site Updates',
description: 'Latest updates from STARSET Mirror Site'
},
{
code: 'zh-CN',
dataDir: 'zh-CN',
title: 'STARSET 镜像站更新',
description: 'STARSET 镜像站的最新更新'
},
{
code: 'zh-Hant',
dataDir: 'zh-TW',
title: 'STARSET 鏡像站更新',
description: 'STARSET 鏡像站的最新更新'
}
];
async function generateRSSFeed(lang: LanguageConfig) {
// Read all updates
const updatesIndex = await fs.readJson(path.join('data', lang.dataDir, 'updates.json'));
const years = updatesIndex.years as string[];
let allUpdates: Update[] = [];
for (const year of years) {
const yearData = await fs.readJson(path.join('data', lang.dataDir, 'updates', year));
allUpdates = [...allUpdates, ...yearData.updates];
}
// Sort updates by date in descending order
allUpdates.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
// Take the latest 10 updates
const latestUpdates = allUpdates.slice(0, 10);
// Create feed
const feed = new Feed({
title: lang.title,
description: lang.description,
id: `${SITE_URL}/${lang.code}/updates`,
link: `${SITE_URL}/${lang.code}/updates`,
language: lang.code,
favicon: `${SITE_URL}/favicon.ico`,
copyright: "All rights reserved",
updated: latestUpdates[0] ? new Date(latestUpdates[0].date) : new Date(),
feedLinks: {
rss2: `${SITE_URL}/${lang.code}/rss.xml`
}
});
// Add items to feed
for (const update of latestUpdates) {
feed.addItem({
title: update.title,
id: update.id,
link: update.link || `${SITE_URL}/${lang.code}/updates#${update.id}`,
description: update.summary,
date: new Date(update.date),
category: update.tags.map(tag => ({ name: tag }))
});
}
// Create dist directory if it doesn't exist
const distPath = path.join('dist', lang.code);
await fs.ensureDir(distPath);
// Write feed to file
await fs.writeFile(
path.join(distPath, 'rss.xml'),
feed.rss2()
);
}
async function main() {
try {
for (const lang of LANGUAGES) {
await generateRSSFeed(lang);
console.log(`Generated RSS feed for ${lang.code}`);
}
} catch (error) {
console.error('Error generating RSS feeds:', error);
process.exit(1);
}
}
main();

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react';
import { RssIcon } from 'lucide-react'; // Add this line
import { Update, useUpdates, getUpdateUrl } from '../utils/updates';
import TagFilter from './ui/TagFilter';
@ -60,6 +61,9 @@ const Pagination: React.FC<PaginationProps> = ({
const Timeline = () => {
const { t, i18n } = useTranslation();
const LANGUAGE_CODE_MAP: Record<string, string> = {
'zh-TW': 'zh-Hant'
};
const [searchParams, setSearchParams] = useSearchParams();
// 获取当前页码和标签筛选
@ -117,16 +121,29 @@ const Timeline = () => {
return (
<div className="container mx-auto px-8 md:px-20 lg:px-32 xl:px-48 2xl:px-64">
<div className="max-w-5xl">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-12 pl-16">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4 md:mb-0">
{t('updates.title')}
</h2>
<div className="flex flex-col md:flex-row md:items-center justify-between mb-12 pl-16 space-y-6 md:space-y-0">
<div className="flex items-center gap-2">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">
{t('updates.title')}
</h2>
<a
href={`/${LANGUAGE_CODE_MAP[i18n.language] || i18n.language}/rss.xml`}
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
title="RSS Feed"
>
<RssIcon size={20} />
</a>
</div>
<TagFilter
availableTags={availableTags}
selectedTags={selectedTags}
onChange={handleTagChange}
/>
<div className="w-full md:w-auto">
<TagFilter
availableTags={availableTags}
selectedTags={selectedTags}
onChange={handleTagChange}
/>
</div>
</div>
<div className="relative">

View file

@ -59,26 +59,26 @@ const TagFilter: React.FC<TagFilterProps> = ({
return (
<div className="relative" ref={dropdownRef}>
<div className="flex flex-wrap items-center gap-2">
<div className="flex flex-wrap items-center gap-2 max-w-full">
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-2 items-center">
<div className="flex flex-wrap gap-2 items-center max-w-full">
{selectedTags.map(tag => (
<span
key={tag}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
>
{t(`updates.tags.${tag}`)}
<button
onClick={() => handleRemoveTag(tag)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<X className="h-4 w-4" />
<X className="h-3.5 w-3.5" />
</button>
</span>
))}
<button
onClick={handleClearAll}
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-sm"
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-sm whitespace-nowrap"
>
{t('updates.filter.clear')}
</button>
@ -86,13 +86,13 @@ const TagFilter: React.FC<TagFilterProps> = ({
)}
<button
onClick={() => setIsOpen(!isOpen)}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
isOpen
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
<Search className="h-4 w-4" />
<Search className="h-3.5 w-3.5" />
{t('updates.filter.title')}
</button>
</div>