feat: enhance theme management and add login form component for user authentication
This commit is contained in:
@@ -46,7 +46,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
// Remove existing theme classes
|
// Remove existing theme classes
|
||||||
root.classList.remove('theme-blue', 'theme-violet', 'dark');
|
root.classList.remove('theme-blue', 'theme-violet', 'theme-red', 'theme-rose', 'theme-orange', 'theme-green', 'theme-yellow', 'dark');
|
||||||
|
|
||||||
// Add new theme class
|
// Add new theme class
|
||||||
root.classList.add(`theme-${theme}`);
|
root.classList.add(`theme-${theme}`);
|
||||||
|
|||||||
279
app/components/start-screen.tsx
Normal file
279
app/components/start-screen.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
|
||||||
|
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
|
||||||
|
import { useTheme } from '@/app/components/ThemeProvider';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export function LoginForm({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
const [step, setStep] = useState<'login' | 'settings'>('login');
|
||||||
|
const { config, updateConfig, testConnection } = useNavidromeConfig();
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
serverUrl: config.serverUrl || '',
|
||||||
|
username: config.username || '',
|
||||||
|
password: config.password || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
|
||||||
|
// Settings for step 2
|
||||||
|
const [scrobblingEnabled, setScrobblingEnabled] = useState(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('lastfm-scrobbling-enabled') === 'true';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestAndNext = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.serverUrl || !formData.username || !formData.password) {
|
||||||
|
toast({
|
||||||
|
title: "Missing Information",
|
||||||
|
description: "Please fill in all fields before proceeding.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTesting(true);
|
||||||
|
try {
|
||||||
|
// Strip trailing slash from server URL before testing
|
||||||
|
const cleanServerUrl = formData.serverUrl.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const success = await testConnection({
|
||||||
|
serverUrl: cleanServerUrl,
|
||||||
|
username: formData.username,
|
||||||
|
password: formData.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Save the config
|
||||||
|
updateConfig({
|
||||||
|
serverUrl: cleanServerUrl,
|
||||||
|
username: formData.username,
|
||||||
|
password: formData.password
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Connection Successful",
|
||||||
|
description: "Connected to Navidrome! Let's configure your preferences.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to settings step
|
||||||
|
setStep('settings');
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Connection Failed",
|
||||||
|
description: "Could not connect to the server. Please check your settings.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Connection Error",
|
||||||
|
description: "An error occurred while testing the connection.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinishSetup = () => {
|
||||||
|
// Save scrobbling preference
|
||||||
|
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Setup Complete",
|
||||||
|
description: "Welcome to mice! Your music streaming experience is ready.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload the page to start the main app
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrobblingToggle = (enabled: boolean) => {
|
||||||
|
setScrobblingEnabled(enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step === 'settings') {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FaPalette className="w-5 h-5" />
|
||||||
|
Customize Your Experience
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure your preferences to get started
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* Theme Selection */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="theme">Theme</Label>
|
||||||
|
<Select value={theme} onValueChange={setTheme}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="blue">Blue</SelectItem>
|
||||||
|
<SelectItem value="violet">Violet</SelectItem>
|
||||||
|
<SelectItem value="red">Red</SelectItem>
|
||||||
|
<SelectItem value="rose">Rose</SelectItem>
|
||||||
|
<SelectItem value="orange">Orange</SelectItem>
|
||||||
|
<SelectItem value="green">Green</SelectItem>
|
||||||
|
<SelectItem value="yellow">Yellow</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Last.fm Scrobbling */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<FaLastfm className="w-4 h-4" />
|
||||||
|
Last.fm Scrobbling
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={scrobblingEnabled ? "enabled" : "disabled"}
|
||||||
|
onValueChange={(value) => handleScrobblingToggle(value === "enabled")}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="enabled">Enabled</SelectItem>
|
||||||
|
<SelectItem value="disabled">Disabled</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{scrobblingEnabled
|
||||||
|
? "Tracks will be scrobbled to Last.fm via Navidrome"
|
||||||
|
: "Last.fm scrobbling will be disabled"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button onClick={handleFinishSetup} className="w-full">
|
||||||
|
<FaCheck className="w-4 h-4 mr-2" />
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setStep('login')}
|
||||||
|
>
|
||||||
|
Back to Connection Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FaServer className="w-5 h-5" />
|
||||||
|
Connect to Navidrome
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your Navidrome server details to get started
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleTestAndNext}>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="serverUrl" className="flex items-center gap-2">
|
||||||
|
Server URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="serverUrl"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://your-navidrome-server.com"
|
||||||
|
value={formData.serverUrl}
|
||||||
|
onChange={(e) => handleInputChange('serverUrl', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="username" className="flex items-center gap-2">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="your-username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="password" className="flex items-center gap-2">
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button type="submit" className="w-full" disabled={isTesting}>
|
||||||
|
{isTesting ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 mr-2 animate-spin rounded-full border-2 border-transparent border-t-current" />
|
||||||
|
Testing Connection...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaCheck className="w-4 h-4 mr-2" />
|
||||||
|
Test Connection & Continue
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
app/page.tsx
32
app/page.tsx
@@ -7,6 +7,7 @@ import { AlbumArtwork } from './components/album-artwork';
|
|||||||
import { useNavidrome } from './components/NavidromeContext';
|
import { useNavidrome } from './components/NavidromeContext';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Album } from '@/lib/navidrome';
|
import { Album } from '@/lib/navidrome';
|
||||||
|
import { LoginForm } from '@/app/components/start-screen';
|
||||||
|
|
||||||
export default function MusicPage() {
|
export default function MusicPage() {
|
||||||
const { albums, isLoading, error } = useNavidrome();
|
const { albums, isLoading, error } = useNavidrome();
|
||||||
@@ -25,20 +26,23 @@ export default function MusicPage() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full px-4 py-6 lg:px-8 pb-24">
|
// <div className="h-full px-4 py-6 lg:px-8 pb-24">
|
||||||
<div className="text-center">
|
// <div className="text-center">
|
||||||
<p className="text-xl font-semibold text-red/50 mb-2">Connection Error</p>
|
// <p className="text-xl font-semibold text-red/50 mb-2">Connection Error</p>
|
||||||
<p className="text-muted-foreground">{error}</p>
|
// <p className="text-muted-foreground">{error}</p>
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
// <p className="text-sm text-muted-foreground mt-2">
|
||||||
If you need to change your settings, please go to the{' '}
|
// If you need to change your settings, please go to the{' '}
|
||||||
<a
|
// <a
|
||||||
href="/settings"
|
// href="/settings"
|
||||||
className="text-sm text-blue-500 hover:underline"
|
// className="text-sm text-blue-500 hover:underline"
|
||||||
>
|
// >
|
||||||
Settings
|
// Settings
|
||||||
</a>
|
// </a>
|
||||||
</p>
|
// </p>
|
||||||
</div>
|
// </div>
|
||||||
|
// </div>
|
||||||
|
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<LoginForm className="max-w-md mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user