Implement Astro features: React Islands, Server Endpoints, i18n, Partytown

Features added:
- React integration with @astrojs/react
- React components: DMRDashboard, MemberSearch
- API endpoints: /api/repeaters.json, /api/dmr-contacts.json, /api/feed/[category].xml
- i18n support for TR/EN locales with LanguageSwitcher component
- Partytown enabled for third-party script optimization
- Admin panel logo fixed (moved to public/images/)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Buğra Canata
2026-01-17 17:30:58 +03:00
parent 214e9733d0
commit 2467d5555f
14 changed files with 1022 additions and 143 deletions
+11
View File
@@ -0,0 +1,11 @@
export async function GET() {
const contacts = [
{ id: '2867241', callsign: 'YM0KTC', name: 'ARC Merkez', location: 'Bölge 0 - İstanbul', type: 'repeater' },
{ id: '2866808', callsign: 'YM1KTC', name: 'İstanbul Röle', location: 'Bölge 1 - İstanbul', type: 'repeater' },
{ id: '2867887', callsign: 'YM2KTR', name: 'Ankara Röle', location: 'Bölge 2 - Ankara', type: 'repeater' },
{ id: '2867234', callsign: 'YM3KTC', name: 'İzmir Röle', location: 'Bölge 3 - İzmir', type: 'repeater' },
{ id: '2867235', callsign: 'YM4KTC', name: 'Antalya Röle', location: 'Bölge 4 - Antalya', type: 'repeater' },
];
return Response.json(contacts);
}
+74
View File
@@ -0,0 +1,74 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
// Helper to slugify Turkish categories
function slugify(str: string): string {
return str
.toLowerCase()
.replace(/ş/g, 's')
.replace(/ı/g, 'i')
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]+/g, '-');
}
export async function getStaticPaths() {
const posts = await getCollection('post');
// Extract all unique categories and create slugified paths
const categoryMap = new Map<string, string>();
posts.forEach(post => {
if (post.data.categories) {
post.data.categories.forEach(cat => {
if (!categoryMap.has(cat)) {
categoryMap.set(cat, slugify(cat));
}
});
}
if (post.data.category) {
if (!categoryMap.has(post.data.category)) {
categoryMap.set(post.data.category, slugify(post.data.category));
}
}
});
// Return paths with both original category name and slug
return Array.from(categoryMap.entries()).map(([category, slug]) => ({
params: { category: slug },
props: { originalCategory: category },
}));
}
export async function GET(context) {
const posts = await getCollection('post');
// Get the original category name from props
const originalCategory = context.props.originalCategory as string;
// Filter posts by category
const filteredPosts = posts
.filter(post =>
post.data.categories?.includes(originalCategory) || post.data.category === originalCategory
)
.filter(post => post.data.publishDate) // Only include posts with publishDate
.sort((a, b) => {
const aDate = a.data.publishDate || new Date(0);
const bDate = b.data.publishDate || new Date(0);
return bDate.valueOf() - aDate.valueOf();
})
.slice(0, 20); // Limit to 20 most recent posts
return rss({
title: `ARC - ${originalCategory} Blog Yazıları`,
description: `Amatör Radyocular Derneği - ${originalCategory} kategorisindeki blog yazıları`,
site: context.site?.toString() || 'https://radio.org.tr',
items: filteredPosts.map(post => ({
title: post.data.title,
pubDate: post.data.publishDate!,
description: post.data.excerpt || '',
link: `/blog/${post.slug}/`,
})),
});
}
+36
View File
@@ -0,0 +1,36 @@
export async function GET() {
const repeaters = [
{
id: 'YM1KTC',
name: 'İstanbul Röle',
frequency: '439.4125+',
offset: '+7.6',
tone: '88.5',
colorCode: '1',
status: 'online',
lastheard: new Date().toISOString(),
},
{
id: 'YM2KTR',
name: 'Ankara Röle',
frequency: '439.4750-',
offset: '-5.0',
tone: '88.5',
colorCode: '1',
status: 'online',
lastheard: new Date(Date.now() - 3600000).toISOString(),
},
{
id: 'YM3KTC',
name: 'İzmir Röle',
frequency: '439.5500+',
offset: '+7.6',
tone: '88.5',
colorCode: '1',
status: 'offline',
lastheard: new Date(Date.now() - 86400000).toISOString(),
},
];
return Response.json(repeaters);
}
+30
View File
@@ -0,0 +1,30 @@
---
import Layout from '~/layouts/PageLayout.astro';
import Hero from '~/components/widgets/Hero.astro';
import DMRDashboard from '~/react/DMRDashboard';
import MemberSearch from '~/react/MemberSearch';
const metadata = {
title: 'DMR Röle Durumu - ARC',
};
---
<Layout metadata={metadata}>
<!-- Hero Widget -->
<Hero
title="📡 DMR Röle Durumu"
subtitle="DMR röle sistemlerimizin anlık durumunu buradan takip edebilirsiniz."
/>
<!-- Member Search -->
<section class="mx-auto max-w-4xl px-4 py-8">
<h2 class="text-2xl font-bold mb-4">🔍 Üye Ara</h2>
<MemberSearch client:load />
</section>
<!-- DMR Dashboard -->
<section class="mx-auto max-w-6xl px-4 py-8">
<h2 class="text-2xl font-bold mb-4">📻 Röle Durumları</h2>
<DMRDashboard client:load />
</section>
</Layout>