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 public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
User-agent: *
Allow: /*
Disallow: /api/
Disallow: /login
Disallow: /settings
Sitemap: https://bittorrented.com/sitemap.xml
19 changes: 16 additions & 3 deletions src/app/live-tv/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* Live TV Page (Server Component)
*
* Server-side auth check - redirects to login if not authenticated.
* Shows login prompt if not authenticated, otherwise shows Live TV content.
*/

import { redirect } from 'next/navigation';
import Link from 'next/link';
import { getCurrentUser } from '@/lib/auth';
import { LiveTvContent } from './live-tv-content';

Expand All @@ -19,7 +19,20 @@ export default async function LiveTvPage(): Promise<React.ReactElement> {
const user = await getCurrentUser();

if (!user) {
redirect('/login?redirect=/live-tv&reason=live-tv');
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 text-center px-4">
<h1 className="text-2xl font-bold text-text-primary">Login Required</h1>
<p className="text-text-secondary max-w-md">
You need to be logged in to access Live TV. Sign in to stream live channels from your IPTV playlists.
</p>
<Link
href="/login?redirect=/live-tv"
className="rounded-lg bg-accent-primary px-6 py-2 text-sm font-medium text-white hover:bg-accent-primary/90 transition-colors"
>
Sign In
</Link>
</div>
);
}

return <LiveTvContent />;
Expand Down
14 changes: 11 additions & 3 deletions src/components/home/quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

import { useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/use-auth';
import Link from 'next/link';
import { MagnetIcon, SearchIcon, MusicIcon, VideoIcon } from '@/components/ui/icons';
import { AddMagnetModal } from '@/components/torrents/add-magnet-modal';
Expand Down Expand Up @@ -63,10 +65,16 @@ function QuickActionCard({

export function QuickActions(): React.ReactElement {
const [isModalOpen, setIsModalOpen] = useState(false);
const { isLoggedIn } = useAuth();
const router = useRouter();

const handleOpenModal = useCallback((): void => {
if (!isLoggedIn) {
router.push('/login');
return;
}
setIsModalOpen(true);
}, []);
}, [isLoggedIn, router]);

const handleCloseModal = useCallback((): void => {
setIsModalOpen(false);
Expand All @@ -78,8 +86,8 @@ export function QuickActions(): React.ReactElement {
<QuickActionCard
onClick={handleOpenModal}
icon={MagnetIcon}
title="Add Torrent"
description="Add a magnet link to start streaming"
title={isLoggedIn ? 'Add Torrent' : 'Add Torrent 🔒'}
description={isLoggedIn ? 'Add a magnet link to start streaming' : 'Login required to add torrents'}
color="accent-primary"
/>
<QuickActionCard
Expand Down
2 changes: 2 additions & 0 deletions src/components/layout/main-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export function MainLayout({ children, className }: MainLayoutProps): React.Reac

{/* Footer */}
<footer className="border-t border-border-primary px-4 py-4 text-center text-sm text-text-secondary md:px-6">
<span>&copy; {new Date().getFullYear()} Media Streamer</span>
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because MainLayout is a client component, rendering new Date().getFullYear() can cause a rare hydration mismatch around New Year (server-rendered year vs client-rendered year). To avoid this, consider hardcoding the year, computing it on the server, or using suppressHydrationWarning on the year span.

Suggested change
<span>&copy; {new Date().getFullYear()} Media Streamer</span>
<span suppressHydrationWarning>&copy; {new Date().getFullYear()} Media Streamer</span>

Copilot uses AI. Check for mistakes.
<span className="mx-2">&middot;</span>
<a
href="mailto:support@bittorrented.com?subject=BitTorrented"
className="text-accent-primary hover:text-accent-primary/80 transition-colors"
Expand Down
15 changes: 15 additions & 0 deletions src/components/layout/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ describe('Sidebar Navigation', () => {
expect(link).toHaveAttribute('href', '/login');
});

it('should show lock icon on auth-required items when not logged in', () => {
render(<Sidebar isLoggedIn={false} />);

// Auth-required items should have a lock icon with "Login required" title
const lockIcons = screen.getAllByTitle('Login required');
expect(lockIcons.length).toBeGreaterThan(0);
});

it('should not show lock icon on auth-required items when logged in and premium', () => {
render(<Sidebar isLoggedIn={true} isPremium={true} />);

const lockIcons = screen.queryAllByTitle('Login required');
expect(lockIcons.length).toBe(0);
});

it('should show My Library link with correct href when user IS logged in', () => {
render(<Sidebar isLoggedIn={true} />);

Expand Down
7 changes: 4 additions & 3 deletions src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,15 @@ function NavSection({ items, pathname, onItemClick, isLoggedIn, isPremium }: Nav
{items.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
// Redirect to login if auth-required and not logged in, or paid feature and not premium
// Show lock icon for auth-required items when not logged in
const needsLogin = (item.requiresAuth && !isLoggedIn) || (item.requiresPaid && !isPremium);
const href = needsLogin ? '/login' : item.href;

return (
<li key={item.href}>
<Link
href={href}
href={needsLogin ? '/login' : item.href}
onClick={onItemClick}
title={needsLogin ? 'Login required' : undefined}
className={cn(
Comment on lines +227 to 236
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsLogin combines both "requiresAuth and not logged in" and "requiresPaid and not premium", but the UI always redirects to /login and sets title="Login required". For logged-in users who aren’t premium, this is incorrect/misleading and sends them to the wrong place. Split this into separate flags (e.g., needsAuth vs needsUpgrade) and use an appropriate destination/message (e.g., pricing/upgrade + "Premium required").

Copilot uses AI. Check for mistakes.
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
Expand All @@ -242,6 +242,7 @@ function NavSection({ items, pathname, onItemClick, isLoggedIn, isPremium }: Nav
>
<Icon size={20} className={isActive ? 'text-accent-primary' : ''} />
<span>{item.label}</span>
{needsLogin ? <span className="ml-auto text-xs opacity-50" title="Login required">🔒</span> : null}
{item.badge ? <span className="ml-auto rounded-full bg-accent-primary px-2 py-0.5 text-xs font-medium text-white">
{item.badge}
</span> : null}
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/use-supported-coins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('useSupportedCoins', () => {

expect(result.current.coins).toEqual(mockCoins);
expect(result.current.error).toBeNull();
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true');
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true', expect.objectContaining({ signal: expect.any(AbortSignal) }));
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook now has special handling for AbortError (10s timeout), but the tests only assert that fetch is called with a signal. Consider adding a test that advances fake timers to trigger the abort and asserts the user-facing timeout message ('Payment methods unavailable...') so the new behavior doesn’t regress.

Copilot uses AI. Check for mistakes.
});

it('should filter active coins by default', async () => {
Expand All @@ -72,7 +72,7 @@ describe('useSupportedCoins', () => {
renderHook(() => useSupportedCoins());

await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true');
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true', expect.objectContaining({ signal: expect.any(AbortSignal) }));
});
});

Expand All @@ -90,7 +90,7 @@ describe('useSupportedCoins', () => {
renderHook(() => useSupportedCoins({ activeOnly: false }));

await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=false');
expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=false', expect.objectContaining({ signal: expect.any(AbortSignal) }));
});
});

Expand Down
12 changes: 10 additions & 2 deletions src/hooks/use-supported-coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ export function useSupportedCoins(
setError(null);

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);

const response = await fetch(
`/api/supported-coins?active_only=${activeOnly}`
`/api/supported-coins?active_only=${activeOnly}`,
{ signal: controller.signal }
);
clearTimeout(timeoutId);
Comment on lines 65 to +73
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeoutId is only cleared after fetch resolves successfully. If fetch rejects (network error, AbortError, JSON parsing error), the timeout remains scheduled and can fire later, holding closures and aborting a controller after the request has already failed. Clear the timeout in finally (and/or immediately after creating it via a scoped helper) so it’s always cleaned up regardless of how the request completes.

Copilot uses AI. Check for mistakes.

const data = (await response.json()) as
| SupportedCoinsResponse
Expand All @@ -79,7 +84,10 @@ export function useSupportedCoins(
const successData = data as SupportedCoinsResponse;
setCoins(successData.coins);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch supported coins');
const message = err instanceof Error && err.name === 'AbortError'
? 'Payment methods unavailable. Please try again later.'
: err instanceof Error ? err.message : 'Failed to fetch supported coins';
setError(message);
setCoins([]);
} finally {
setIsLoading(false);
Expand Down
Loading