Proyecto App Windows
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Page Not Found | IPTVnator"
|
||||
description="The page you're looking for doesn't exist."
|
||||
noindex={true}
|
||||
>
|
||||
<section class="flex min-h-[80vh] items-center justify-center pt-20">
|
||||
<div class="mx-auto max-w-md px-6 text-center">
|
||||
<img
|
||||
src="/iptvnator/images/broken-tv.svg"
|
||||
alt="Broken TV"
|
||||
class="mx-auto h-48 w-48 opacity-40"
|
||||
/>
|
||||
<h1 class="mt-8 font-display text-5xl font-bold text-surface-50">404</h1>
|
||||
<p class="mt-4 text-lg text-surface-400">
|
||||
This page doesn't exist. Maybe the channel was removed from the playlist.
|
||||
</p>
|
||||
<a
|
||||
href="/iptvnator/"
|
||||
class="mt-8 inline-flex items-center gap-2.5 rounded-lg bg-accent-500 px-6 py-3 text-sm font-semibold text-surface-950 transition-all hover:bg-accent-400"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Go back home
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
const posts = await getCollection('blog');
|
||||
return posts
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
---
|
||||
|
||||
<BlogPost
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
pubDate={post.data.pubDate}
|
||||
updatedDate={post.data.updatedDate}
|
||||
author={post.data.author}
|
||||
tags={post.data.tags}
|
||||
heroImage={post.data.heroImage}
|
||||
>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import FeaturedBlogPost from '../../components/FeaturedBlogPost.astro';
|
||||
import BlogPostCard from '../../components/BlogPostCard.astro';
|
||||
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const featuredPost = posts.find((post) => post.data.featured) ?? posts[0];
|
||||
const remainingPosts = featuredPost ? posts.filter((post) => post.id !== featuredPost.id) : [];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Blog | IPTVnator"
|
||||
description="Release notes, tutorials, and project updates from the IPTVnator team."
|
||||
keywords={['IPTVnator blog', 'IPTV release notes', 'EPG, Archive setup', 'Xtream Codes Player (XC)', 'Stalker Player Support (STB)']}
|
||||
>
|
||||
<section class="pt-36 pb-24">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<!-- Header -->
|
||||
<div class="max-w-2xl">
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">
|
||||
Blog
|
||||
</span>
|
||||
<h1 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
|
||||
Blog
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-surface-400">
|
||||
Release notes, tutorials, and project updates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{posts.length > 0 ? (
|
||||
<>
|
||||
{/* Featured post */}
|
||||
{featuredPost && (
|
||||
<div class="mt-14">
|
||||
<FeaturedBlogPost
|
||||
title={featuredPost.data.title}
|
||||
description={featuredPost.data.description}
|
||||
pubDate={featuredPost.data.pubDate}
|
||||
slug={featuredPost.id}
|
||||
tags={featuredPost.data.tags}
|
||||
heroImage={featuredPost.data.heroImage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remaining posts grid */}
|
||||
{remainingPosts.length > 0 && (
|
||||
<div class="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{remainingPosts.map((post) => (
|
||||
<BlogPostCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
pubDate={post.data.pubDate}
|
||||
slug={post.id}
|
||||
tags={post.data.tags}
|
||||
heroImage={post.data.heroImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p class="mt-14 text-center text-surface-500">No posts yet. Check back soon!</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Hero from '../components/Hero.astro';
|
||||
import FeatureGrid from '../components/FeatureGrid.astro';
|
||||
import ScreenshotShowcase from '../components/ScreenshotShowcase.astro';
|
||||
import DownloadCTA from '../components/DownloadCTA.astro';
|
||||
import SupportSection from '../components/SupportSection.astro';
|
||||
import FeaturedBlogPost from '../components/FeaturedBlogPost.astro';
|
||||
import BlogPostCard from '../components/BlogPostCard.astro';
|
||||
|
||||
const includeDrafts = import.meta.env.DEV || import.meta.env.PUBLIC_INCLUDE_DRAFTS === 'true';
|
||||
|
||||
const allPosts = (await getCollection('blog'))
|
||||
.filter((post) => includeDrafts || !post.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const featuredPost = allPosts.find((post) => post.data.featured) ?? allPosts[0];
|
||||
const otherPosts = featuredPost ? allPosts.filter((post) => post.id !== featuredPost.id).slice(0, 2) : [];
|
||||
const posts = featuredPost ? [featuredPost, ...otherPosts] : allPosts.slice(0, 3);
|
||||
|
||||
const homeSchema = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'IPTVnator',
|
||||
url: 'https://4gray.github.io/iptvnator/',
|
||||
description:
|
||||
'A cross-platform IPTV player supporting M3U/M3U8 playlists, Xtream Codes API, and Stalker portals.',
|
||||
inLanguage: 'en',
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'IPTVnator',
|
||||
applicationCategory: 'MultimediaApplication',
|
||||
operatingSystem: 'Windows, macOS, Linux, Web',
|
||||
softwareVersion: '0.19.0',
|
||||
url: 'https://4gray.github.io/iptvnator/',
|
||||
downloadUrl: 'https://github.com/4gray/iptvnator/releases/latest',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: '4gray',
|
||||
url: 'https://github.com/4gray',
|
||||
},
|
||||
sameAs: [
|
||||
'https://github.com/4gray/iptvnator',
|
||||
'https://t.me/iptvnator',
|
||||
'https://ko-fi.com/4gray',
|
||||
],
|
||||
description:
|
||||
'Open-source IPTV player with M3U/M3U8, Xtream Codes API, and Stalker portal support.',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="IPTVnator - Open Source IPTV Player"
|
||||
description="A cross-platform IPTV player supporting M3U/M3U8 playlists, Xtream Codes API, and Stalker portals. Available as a desktop app and PWA."
|
||||
keywords={['IPTV player', 'M3U', 'M3U8', 'Xtream Codes', 'Stalker portal', 'EPG', 'XMLTV', 'open-source']}
|
||||
jsonLd={homeSchema}
|
||||
>
|
||||
<Hero />
|
||||
<FeatureGrid />
|
||||
<ScreenshotShowcase />
|
||||
<DownloadCTA />
|
||||
<SupportSection />
|
||||
|
||||
<!-- Recent Blog Posts -->
|
||||
{
|
||||
posts.length > 0 && (
|
||||
<section class="py-28">
|
||||
<hr class="section-divider mb-28" />
|
||||
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">
|
||||
Blog
|
||||
</span>
|
||||
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
|
||||
Latest <em class="font-medium text-accent-400" style="font-style: italic;">updates</em>
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-surface-400">
|
||||
Release notes, tutorials, and project updates.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/iptvnator/blog/"
|
||||
class="hidden items-center gap-1.5 font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors hover:text-accent-400 sm:inline-flex"
|
||||
>
|
||||
View all posts
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Featured post */}
|
||||
{featuredPost && (
|
||||
<div class="mt-12">
|
||||
<FeaturedBlogPost
|
||||
title={featuredPost.data.title}
|
||||
description={featuredPost.data.description}
|
||||
pubDate={featuredPost.data.pubDate}
|
||||
slug={featuredPost.id}
|
||||
tags={featuredPost.data.tags}
|
||||
heroImage={featuredPost.data.heroImage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other posts grid */}
|
||||
{otherPosts.length > 0 && (
|
||||
<div class="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{otherPosts.map((post) => (
|
||||
<BlogPostCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
pubDate={post.data.pubDate}
|
||||
slug={post.id}
|
||||
tags={post.data.tags}
|
||||
heroImage={post.data.heroImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user