@@ -2,7 +2,8 @@
|
|||||||
NEXT_PUBLIC_NAVIDROME_URL=http://localhost:4533
|
NEXT_PUBLIC_NAVIDROME_URL=http://localhost:4533
|
||||||
NEXT_PUBLIC_NAVIDROME_USERNAME=your_username
|
NEXT_PUBLIC_NAVIDROME_USERNAME=your_username
|
||||||
NEXT_PUBLIC_NAVIDROME_PASSWORD=your_password
|
NEXT_PUBLIC_NAVIDROME_PASSWORD=your_password
|
||||||
|
NEXT_PUBLIC_POSTHOG_KEY=KEY
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST=HOSTURL
|
||||||
# Example for external server:
|
# Example for external server:
|
||||||
# NEXT_PUBLIC_NAVIDROME_URL=https://your-navidrome-server.com
|
# NEXT_PUBLIC_NAVIDROME_URL=https://your-navidrome-server.com
|
||||||
# NEXT_PUBLIC_NAVIDROME_USERNAME=your_username
|
# NEXT_PUBLIC_NAVIDROME_USERNAME=your_username
|
||||||
|
|||||||
34
.github/workflows/jest.yml
vendored
34
.github/workflows/jest.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Run Jest Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'apps/data/albums.ts'
|
|
||||||
- 'apps/data/artists.ts'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'apps/data/albums.ts'
|
|
||||||
- 'apps/data/artists.ts'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Run Jest tests
|
|
||||||
run: npm test
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# Migration Summary: Firebase → Navidrome/Subsonic
|
|
||||||
|
|
||||||
## ✅ Completed Migration Tasks
|
|
||||||
|
|
||||||
### 🗑️ Removed Legacy Systems
|
|
||||||
- [x] **Firebase Dependencies**: Removed firebase, react-firebase-hooks packages
|
|
||||||
- [x] **Static Data Files**: Moved `app/data/` (albums.ts, artists.ts, playlists.ts) to backup
|
|
||||||
- [x] **Firebase Config**: Moved `app/firebase/` directory to backup
|
|
||||||
- [x] **Authentication System**: Removed Firebase Auth integration
|
|
||||||
- [x] **Database Connections**: Removed Firestore database calls
|
|
||||||
|
|
||||||
### 🚀 Implemented Navidrome Integration
|
|
||||||
- [x] **Navidrome API Client** (`lib/navidrome.ts`)
|
|
||||||
- Subsonic API authentication with token-based security
|
|
||||||
- All major endpoints: ping, getArtists, getAlbums, getAlbum, search3, etc.
|
|
||||||
- Stream URL generation for audio playback
|
|
||||||
- Cover art URL generation with size parameters
|
|
||||||
- Star/unstar functionality for favorites
|
|
||||||
- Scrobbling support for play tracking
|
|
||||||
|
|
||||||
- [x] **React Context Provider** (`app/components/NavidromeContext.tsx`)
|
|
||||||
- Global state management for music library data
|
|
||||||
- Loading states for UI feedback
|
|
||||||
- Error handling and connection testing
|
|
||||||
- Data fetching with automatic refresh
|
|
||||||
- CRUD operations for playlists
|
|
||||||
|
|
||||||
### 🎵 Updated Audio System
|
|
||||||
- [x] **AudioPlayerContext** - Completely rewritten for Navidrome
|
|
||||||
- Real audio streaming instead of static file URLs
|
|
||||||
- Queue management with Navidrome song objects
|
|
||||||
- Automatic scrobbling when tracks play
|
|
||||||
- Track conversion from Navidrome Song to playable Track format
|
|
||||||
|
|
||||||
- [x] **AudioPlayer Component**
|
|
||||||
- Updated to handle Navidrome streaming URLs
|
|
||||||
- Dynamic cover art from Navidrome getCoverArt API
|
|
||||||
- Proper track metadata display (artist, album, duration)
|
|
||||||
|
|
||||||
### 🎨 Updated UI Components
|
|
||||||
- [x] **AlbumArtwork Component**
|
|
||||||
- Uses Navidrome Album interface
|
|
||||||
- Dynamic cover art with getCoverArt API
|
|
||||||
- Context menu integration with Navidrome playlists
|
|
||||||
- Proper album metadata display (year, genre, song count)
|
|
||||||
|
|
||||||
- [x] **ArtistIcon Component**
|
|
||||||
- Uses Navidrome Artist interface
|
|
||||||
- Artist cover art support
|
|
||||||
- Album count display
|
|
||||||
- Star/unstar functionality in context menu
|
|
||||||
|
|
||||||
### 📄 Updated Pages
|
|
||||||
- [x] **Main Page** (`app/page.tsx`)
|
|
||||||
- Uses NavidromeContext for album data
|
|
||||||
- Loading states with skeleton UI
|
|
||||||
- Error handling for connection issues
|
|
||||||
- Recent and newest album sections
|
|
||||||
|
|
||||||
- [x] **Album Detail Page** (`app/album/[id]/page.tsx`)
|
|
||||||
- Fetches album and songs from Navidrome
|
|
||||||
- Real-time song playback with streaming
|
|
||||||
- Star/unstar album functionality
|
|
||||||
- Proper track listing with metadata
|
|
||||||
|
|
||||||
- [x] **Artist Page** (`app/artist/[artist]/page.tsx`)
|
|
||||||
- Artist details from Navidrome API
|
|
||||||
- Dynamic album grid for artist
|
|
||||||
- Star/unstar artist functionality
|
|
||||||
- Modern gradient header design
|
|
||||||
|
|
||||||
- [x] **Library Pages**
|
|
||||||
- `app/library/albums/page.tsx` - Shows all albums in grid layout
|
|
||||||
- `app/library/artists/page.tsx` - Shows all artists in grid layout
|
|
||||||
- `app/library/playlists/page.tsx` - Playlist management with CRUD operations
|
|
||||||
|
|
||||||
### 🔧 Configuration & Documentation
|
|
||||||
- [x] **Environment Configuration**
|
|
||||||
- `.env.example` with Navidrome connection settings
|
|
||||||
- Removed Firebase environment variables from package.json
|
|
||||||
|
|
||||||
- [x] **Documentation**
|
|
||||||
- `NAVIDROME_MIGRATION.md` - Detailed migration guide
|
|
||||||
- Updated `README.md` with new setup instructions
|
|
||||||
- Feature documentation and troubleshooting
|
|
||||||
|
|
||||||
- [x] **Type Safety**
|
|
||||||
- TypeScript interfaces matching Subsonic API responses
|
|
||||||
- Proper error handling throughout the application
|
|
||||||
- Type-safe component props and context values
|
|
||||||
|
|
||||||
### 🧪 Testing
|
|
||||||
- [x] **Test Suite** (`__tests__/navidrome.test.ts`)
|
|
||||||
- API client functionality tests
|
|
||||||
- TypeScript interface validation
|
|
||||||
- URL generation testing
|
|
||||||
- Configuration validation
|
|
||||||
|
|
||||||
## 🎯 Key Benefits Achieved
|
|
||||||
|
|
||||||
### **Real Music Streaming**
|
|
||||||
- Replaced static MP3 URLs with dynamic Navidrome streaming
|
|
||||||
- Support for multiple audio formats and bitrates
|
|
||||||
- Proper audio metadata from music files
|
|
||||||
|
|
||||||
### **Dynamic Library**
|
|
||||||
- No more manual JSON file management
|
|
||||||
- Auto-discovery of new music added to Navidrome
|
|
||||||
- Real-time library updates
|
|
||||||
|
|
||||||
### **Enhanced Features**
|
|
||||||
- Scrobbling for play tracking
|
|
||||||
- Star/favorite functionality
|
|
||||||
- Playlist management (create, edit, delete)
|
|
||||||
- Search across entire music library
|
|
||||||
- High-quality album artwork
|
|
||||||
|
|
||||||
### **Better Architecture**
|
|
||||||
- Removed Firebase dependency completely
|
|
||||||
- Self-hosted music solution
|
|
||||||
- Standards-based Subsonic API integration
|
|
||||||
- Type-safe development with proper interfaces
|
|
||||||
|
|
||||||
## 🔄 Migration Path
|
|
||||||
|
|
||||||
1. **Backup**: Old Firebase and static data moved to `-old` directories
|
|
||||||
2. **Dependencies**: Firebase packages removed, crypto built-in used
|
|
||||||
3. **Environment**: New `.env.local` needed with Navidrome credentials
|
|
||||||
4. **Data Flow**: `Static JSON → Firebase → Navidrome API`
|
|
||||||
5. **Authentication**: `Firebase Auth → Navidrome Server Authentication`
|
|
||||||
6. **Streaming**: `Static Files → Navidrome Stream API`
|
|
||||||
|
|
||||||
## 🚦 Ready for Production
|
|
||||||
|
|
||||||
The application is now fully migrated and ready for use with any Navidrome server. All core functionality has been preserved and enhanced:
|
|
||||||
|
|
||||||
- ✅ Browse music library (albums, artists, songs)
|
|
||||||
- ✅ Audio playback with queue management
|
|
||||||
- ✅ Search functionality
|
|
||||||
- ✅ Playlist management
|
|
||||||
- ✅ Favorites/starring
|
|
||||||
- ✅ Responsive design
|
|
||||||
- ✅ Error handling and loading states
|
|
||||||
|
|
||||||
**Next Steps**: Set up Navidrome server and configure connection in `.env.local`
|
|
||||||
@@ -10,9 +10,10 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
export const AudioPlayer: React.FC = () => {
|
export const AudioPlayer: React.FC = () => {
|
||||||
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue } = useAudioPlayer();
|
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue } = useAudioPlayer();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
|
const preloadAudioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [showVolumeSlider, setShowVolumeSlider] = useState(false);
|
const [showVolumeSlider, setShowVolumeSlider] = useState(false);
|
||||||
@@ -31,6 +32,19 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
|
|
||||||
|
// Load saved volume
|
||||||
|
const savedVolume = localStorage.getItem('navidrome-volume');
|
||||||
|
if (savedVolume) {
|
||||||
|
try {
|
||||||
|
const volumeValue = parseFloat(savedVolume);
|
||||||
|
if (volumeValue >= 0 && volumeValue <= 1) {
|
||||||
|
setVolume(volumeValue);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse saved volume:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up old localStorage entries with track IDs
|
// Clean up old localStorage entries with track IDs
|
||||||
const keysToRemove: string[] = [];
|
const keysToRemove: string[] = [];
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
@@ -42,6 +56,16 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Apply volume to audio element when volume changes
|
||||||
|
useEffect(() => {
|
||||||
|
const audioCurrent = audioRef.current;
|
||||||
|
if (audioCurrent) {
|
||||||
|
audioCurrent.volume = volume;
|
||||||
|
}
|
||||||
|
// Save volume to localStorage
|
||||||
|
localStorage.setItem('navidrome-volume', volume.toString());
|
||||||
|
}, [volume]);
|
||||||
|
|
||||||
// Save position when component unmounts or track changes
|
// Save position when component unmounts or track changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audioCurrent = audioRef.current;
|
const audioCurrent = audioRef.current;
|
||||||
@@ -248,9 +272,6 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newVolume = parseFloat(e.target.value);
|
const newVolume = parseFloat(e.target.value);
|
||||||
setVolume(newVolume);
|
setVolume(newVolume);
|
||||||
if (audioCurrent) {
|
|
||||||
audioCurrent.volume = newVolume;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
@@ -304,6 +325,7 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<audio ref={audioRef} hidden />
|
<audio ref={audioRef} hidden />
|
||||||
|
<audio ref={preloadAudioRef} hidden preload="metadata" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -357,6 +379,7 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<audio ref={audioRef} hidden />
|
<audio ref={audioRef} hidden />
|
||||||
|
<audio ref={preloadAudioRef} hidden preload="metadata" />
|
||||||
|
|
||||||
{/* Full Screen Player */}
|
{/* Full Screen Player */}
|
||||||
<FullScreenPlayer
|
<FullScreenPlayer
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
const savedCurrentTrack = localStorage.getItem('navidrome-currentTrack');
|
const savedCurrentTrack = localStorage.getItem('navidrome-currentTrack');
|
||||||
if (savedCurrentTrack) {
|
if (savedCurrentTrack) {
|
||||||
try {
|
try {
|
||||||
setCurrentTrack(JSON.parse(savedCurrentTrack));
|
const track = JSON.parse(savedCurrentTrack);
|
||||||
|
// Clear autoPlay flag when loading from localStorage to prevent auto-play on refresh
|
||||||
|
track.autoPlay = false;
|
||||||
|
setCurrentTrack(track);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse saved current track:', error);
|
console.error('Failed to parse saved current track:', error);
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,9 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentTrack) {
|
if (currentTrack) {
|
||||||
localStorage.setItem('navidrome-currentTrack', JSON.stringify(currentTrack));
|
// Remove autoPlay flag when saving to localStorage
|
||||||
|
const { autoPlay, ...trackToSave } = currentTrack;
|
||||||
|
localStorage.setItem('navidrome-currentTrack', JSON.stringify(trackToSave));
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('navidrome-currentTrack');
|
localStorage.removeItem('navidrome-currentTrack');
|
||||||
}
|
}
|
||||||
@@ -282,11 +287,11 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Play the first shuffled track and set the rest as queue
|
// Play the first shuffled track and set the rest as queue
|
||||||
playTrack(shuffledTracks[0]);
|
playTrack(shuffledTracks[0], true); // Enable autoplay
|
||||||
setQueue(shuffledTracks.slice(1));
|
setQueue(shuffledTracks.slice(1));
|
||||||
} else {
|
} else {
|
||||||
// Normal order: play first track and set the rest as queue
|
// Normal order: play first track and set the rest as queue
|
||||||
playTrack(tracks[0]);
|
playTrack(tracks[0], true); // Enable autoplay
|
||||||
setQueue(tracks.slice(1));
|
setQueue(tracks.slice(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,11 +337,11 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
}
|
}
|
||||||
|
|
||||||
setQueue(remainingTracks);
|
setQueue(remainingTracks);
|
||||||
playTrack(tracks[startingIndex]);
|
playTrack(tracks[startingIndex], true); // Enable autoplay
|
||||||
} else {
|
} else {
|
||||||
// Normal order: set the remaining tracks after the starting track as queue
|
// Normal order: set the remaining tracks after the starting track as queue
|
||||||
setQueue(tracks.slice(startingIndex + 1));
|
setQueue(tracks.slice(startingIndex + 1));
|
||||||
playTrack(tracks[startingIndex]);
|
playTrack(tracks[startingIndex], true); // Enable autoplay
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -360,8 +365,8 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
const targetTrack = queue[index];
|
const targetTrack = queue[index];
|
||||||
// Remove all tracks before the target track (including the target track)
|
// Remove all tracks before the target track (including the target track)
|
||||||
setQueue((prevQueue) => prevQueue.slice(index + 1));
|
setQueue((prevQueue) => prevQueue.slice(index + 1));
|
||||||
// Play the target track
|
// Play the target track with autoplay enabled
|
||||||
playTrack(targetTrack);
|
playTrack(targetTrack, true);
|
||||||
}
|
}
|
||||||
}, [queue, playTrack]);
|
}, [queue, playTrack]);
|
||||||
|
|
||||||
@@ -445,11 +450,11 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Play the first shuffled track and set the rest as queue
|
// Play the first shuffled track and set the rest as queue
|
||||||
playTrack(shuffledTracks[0]);
|
playTrack(shuffledTracks[0], true); // Enable autoplay
|
||||||
setQueue(shuffledTracks.slice(1));
|
setQueue(shuffledTracks.slice(1));
|
||||||
} else {
|
} else {
|
||||||
// Normal order: play first track and set the rest as queue
|
// Normal order: play first track and set the rest as queue
|
||||||
playTrack(allTracks[0]);
|
playTrack(allTracks[0], true); // Enable autoplay
|
||||||
setQueue(allTracks.slice(1));
|
setQueue(allTracks.slice(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
app/components/DynamicViewportTheme.tsx
Normal file
8
app/components/DynamicViewportTheme.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useViewportThemeColor } from '@/hooks/use-viewport-theme-color';
|
||||||
|
|
||||||
|
export default function DynamicViewportTheme() {
|
||||||
|
useViewportThemeColor();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import Image from 'next/image';
|
|||||||
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { lrcLibClient } from '@/lib/lrclib';
|
import { lrcLibClient } from '@/lib/lrclib';
|
||||||
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
FaPlay,
|
FaPlay,
|
||||||
FaPause,
|
FaPause,
|
||||||
@@ -333,19 +334,15 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
<h1 className="text-lg sm:text-xl lg:text-3xl font-bold text-foreground mb-2 line-clamp-2 leading-tight">
|
<h1 className="text-lg sm:text-xl lg:text-3xl font-bold text-foreground mb-2 line-clamp-2 leading-tight">
|
||||||
{currentTrack.name}
|
{currentTrack.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-base sm:text-lg lg:text-xl text-foreground/80 mb-1 line-clamp-1">{currentTrack.artist}</p>
|
<Link href={`/album/${currentTrack.artistId}`} className="text-base sm:text-lg lg:text-xl text-foreground/80 mb-1 line-clamp-1">
|
||||||
|
{currentTrack.artist}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
<div className="w-full max-w-sm lg:max-w-md mb-4 lg:mb-6 px-4 flex-shrink-0">
|
<div className="w-full max-w-sm lg:max-w-md mb-4 lg:mb-6 px-4 flex-shrink-0">
|
||||||
<div
|
<div className="w-full" onClick={handleSeek}>
|
||||||
className="h-2 bg-white/20 rounded-full cursor-pointer relative overflow-hidden"
|
<Progress value={progress} className="h-2 cursor-pointer" />
|
||||||
onClick={handleSeek}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-full bg-foreground transition-all duration-150"
|
|
||||||
style={{ width: `${progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm text-foreground/60 mt-2">
|
<div className="flex justify-between text-sm text-foreground/60 mt-2">
|
||||||
<span>{formatTime(currentTime)}</span>
|
<span>{formatTime(currentTime)}</span>
|
||||||
|
|||||||
50
app/components/PostHogProvider.tsx
Normal file
50
app/components/PostHogProvider.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import posthog from "posthog-js"
|
||||||
|
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react"
|
||||||
|
import { Suspense, useEffect } from "react"
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
|
function PathnameTracker() {
|
||||||
|
const posthogClient = usePostHog()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (posthogClient) {
|
||||||
|
posthogClient.capture('$pageview', {
|
||||||
|
path: pathname + (searchParams.toString() ? `?${searchParams.toString()}` : ''),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [posthogClient, pathname, searchParams])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function SuspendedPostHogPageView() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PathnameTracker />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
useEffect(() => {
|
||||||
|
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||||
|
api_host: "/ingest",
|
||||||
|
ui_host: "https://us.posthog.com",
|
||||||
|
capture_pageview: 'history_change',
|
||||||
|
capture_pageleave: true,
|
||||||
|
capture_exceptions: true,
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PHProvider client={posthog}>
|
||||||
|
<SuspendedPostHogPageView />
|
||||||
|
{children}
|
||||||
|
</PHProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AudioPlayerProvider } from "./components/AudioPlayerContext";
|
import { AudioPlayerProvider } from "./components/AudioPlayerContext";
|
||||||
import { NavidromeProvider } from "./components/NavidromeContext";
|
import { NavidromeProvider } from "./components/NavidromeContext";
|
||||||
import { NavidromeConfigProvider } from "./components/NavidromeConfigContext";
|
import { NavidromeConfigProvider } from "./components/NavidromeConfigContext";
|
||||||
import { ThemeProvider } from "./components/ThemeProvider";
|
import { ThemeProvider } from "./components/ThemeProvider";
|
||||||
|
import { PostHogProvider } from "./components/PostHogProvider";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import type { Viewport } from 'next';
|
|
||||||
import Ihateserverside from './components/ihateserverside';
|
import Ihateserverside from './components/ihateserverside';
|
||||||
|
import DynamicViewportTheme from './components/DynamicViewportTheme';
|
||||||
export const viewport: Viewport = {
|
|
||||||
themeColor: 'black',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
@@ -71,25 +66,37 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set initial theme color based on theme
|
||||||
|
const themeColors = {
|
||||||
|
blue: '#0f0f23',
|
||||||
|
violet: '#0c0a2e'
|
||||||
|
};
|
||||||
|
|
||||||
|
const metaThemeColor = document.createElement('meta');
|
||||||
|
metaThemeColor.name = 'theme-color';
|
||||||
|
metaThemeColor.content = themeColors[theme];
|
||||||
|
document.head.appendChild(metaThemeColor);
|
||||||
})();
|
})();
|
||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background`}>
|
||||||
<ThemeProvider>
|
<PostHogProvider>
|
||||||
<NavidromeConfigProvider>
|
<ThemeProvider>
|
||||||
<NavidromeProvider>
|
<DynamicViewportTheme />
|
||||||
<AudioPlayerProvider>
|
<NavidromeConfigProvider>
|
||||||
<SpeedInsights />
|
<NavidromeProvider>
|
||||||
<Analytics />
|
<AudioPlayerProvider>
|
||||||
<Ihateserverside>
|
<Ihateserverside>
|
||||||
{children}
|
{children}
|
||||||
</Ihateserverside>
|
</Ihateserverside>
|
||||||
</AudioPlayerProvider>
|
</AudioPlayerProvider>
|
||||||
</NavidromeProvider>
|
</NavidromeProvider>
|
||||||
</NavidromeConfigProvider>
|
</NavidromeConfigProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</PostHogProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
29
hooks/use-viewport-theme-color.ts
Normal file
29
hooks/use-viewport-theme-color.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTheme } from '@/app/components/ThemeProvider';
|
||||||
|
import { getThemeBackgroundColor } from '@/lib/theme-colors';
|
||||||
|
|
||||||
|
export function useViewportThemeColor() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update the theme-color meta tag dynamically
|
||||||
|
const updateThemeColor = () => {
|
||||||
|
const themeColor = getThemeBackgroundColor(theme);
|
||||||
|
|
||||||
|
// Find existing theme-color meta tag or create one
|
||||||
|
let metaThemeColor = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement;
|
||||||
|
|
||||||
|
if (!metaThemeColor) {
|
||||||
|
metaThemeColor = document.createElement('meta');
|
||||||
|
metaThemeColor.name = 'theme-color';
|
||||||
|
document.head.appendChild(metaThemeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
metaThemeColor.content = themeColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateThemeColor();
|
||||||
|
}, [theme]);
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^@/(.*)$': '<rootDir>/$1',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
11
lib/posthog.ts
Normal file
11
lib/posthog.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { PostHog } from "posthog-node"
|
||||||
|
|
||||||
|
export default function PostHogClient() {
|
||||||
|
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||||
|
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||||
|
capture_pageview: 'history_change',
|
||||||
|
flushAt: 1,
|
||||||
|
flushInterval: 0,
|
||||||
|
})
|
||||||
|
return posthogClient
|
||||||
|
}
|
||||||
29
lib/theme-colors.ts
Normal file
29
lib/theme-colors.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Theme color utilities for dynamic viewport theme color
|
||||||
|
|
||||||
|
export const themeColors = {
|
||||||
|
blue: {
|
||||||
|
background: 'hsl(240, 10%, 3.9%)', // Dark blue background
|
||||||
|
hex: '#09090b' // Hex equivalent for theme-color
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
background: 'hsl(224, 71.4%, 4.1%)', // Dark violet background
|
||||||
|
hex: '#030712' // Hex equivalent for theme-color
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Theme = keyof typeof themeColors;
|
||||||
|
|
||||||
|
export function getThemeBackgroundColor(theme: Theme): string {
|
||||||
|
return themeColors[theme].hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hslToHex(h: number, s: number, l: number): string {
|
||||||
|
l /= 100;
|
||||||
|
const a = s * Math.min(l, 1 - l) / 100;
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color).toString(16).padStart(2, '0');
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
}
|
||||||
@@ -46,6 +46,24 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/ingest/static/:path*',
|
||||||
|
destination: 'https://us-assets.i.posthog.com/static/:path*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/ingest/:path*',
|
||||||
|
destination: 'https://us.i.posthog.com/:path*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/ingest/decide',
|
||||||
|
destination: 'https://us.i.posthog.com/decide',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
// This is required to support PostHog trailing slash API requests
|
||||||
|
skipTrailingSlashRedirect: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"dev": "next dev -p 40625",
|
"dev": "next dev -p 40625",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start -p 40625",
|
"start": "next start -p 40625",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"test": "jest"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
@@ -25,8 +24,6 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@vercel/analytics": "^1.4.1",
|
|
||||||
"@vercel/speed-insights": "^1.1.0",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@@ -34,6 +31,8 @@
|
|||||||
"colorthief": "^2.6.0",
|
"colorthief": "^2.6.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^15.0.3",
|
"next": "^15.0.3",
|
||||||
|
"posthog-js": "^1.255.0",
|
||||||
|
"posthog-node": "^5.1.1",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
@@ -49,7 +48,6 @@
|
|||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"eslint": "^9.17",
|
"eslint": "^9.17",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
|
|||||||
118
pnpm-lock.yaml
generated
118
pnpm-lock.yaml
generated
@@ -53,12 +53,6 @@ importers:
|
|||||||
'@radix-ui/react-toast':
|
'@radix-ui/react-toast':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 1.2.4(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
'@vercel/analytics':
|
|
||||||
specifier: ^1.4.1
|
|
||||||
version: 1.4.1(next@15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
|
||||||
'@vercel/speed-insights':
|
|
||||||
specifier: ^1.1.0
|
|
||||||
version: 1.1.0(next@15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.7
|
specifier: ^1.7.7
|
||||||
version: 1.7.9
|
version: 1.7.9
|
||||||
@@ -80,6 +74,12 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: ^15.0.3
|
specifier: ^15.0.3
|
||||||
version: 15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
posthog-js:
|
||||||
|
specifier: ^1.255.0
|
||||||
|
version: 1.255.0
|
||||||
|
posthog-node:
|
||||||
|
specifier: ^5.1.1
|
||||||
|
version: 5.1.1
|
||||||
react:
|
react:
|
||||||
specifier: ^19
|
specifier: ^19
|
||||||
version: 19.0.0
|
version: 19.0.0
|
||||||
@@ -120,9 +120,6 @@ importers:
|
|||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: 15.1.4
|
specifier: 15.1.4
|
||||||
version: 15.1.4(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)
|
version: 15.1.4(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)
|
||||||
jest:
|
|
||||||
specifier: ^29.7.0
|
|
||||||
version: 29.7.0(@types/node@22.10.5)
|
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8
|
specifier: ^8
|
||||||
version: 8.4.49
|
version: 8.4.49
|
||||||
@@ -1212,55 +1209,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==}
|
resolution: {integrity: sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@vercel/analytics@1.4.1':
|
|
||||||
resolution: {integrity: sha512-ekpL4ReX2TH3LnrRZTUKjHHNpNy9S1I7QmS+g/RQXoSUQ8ienzosuX7T9djZ/s8zPhBx1mpHP/Rw5875N+zQIQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@remix-run/react': ^2
|
|
||||||
'@sveltejs/kit': ^1 || ^2
|
|
||||||
next: '>= 13'
|
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
|
||||||
svelte: '>= 4'
|
|
||||||
vue: ^3
|
|
||||||
vue-router: ^4
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@remix-run/react':
|
|
||||||
optional: true
|
|
||||||
'@sveltejs/kit':
|
|
||||||
optional: true
|
|
||||||
next:
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
svelte:
|
|
||||||
optional: true
|
|
||||||
vue:
|
|
||||||
optional: true
|
|
||||||
vue-router:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@vercel/speed-insights@1.1.0':
|
|
||||||
resolution: {integrity: sha512-rAXxuhhO4mlRGC9noa5F7HLMtGg8YF1zAN6Pjd1Ny4pII4cerhtwSG4vympbCl+pWkH7nBS9kVXRD4FAn54dlg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@sveltejs/kit': ^1 || ^2
|
|
||||||
next: '>= 13'
|
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
|
||||||
svelte: '>= 4'
|
|
||||||
vue: ^3
|
|
||||||
vue-router: ^4
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@sveltejs/kit':
|
|
||||||
optional: true
|
|
||||||
next:
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
svelte:
|
|
||||||
optional: true
|
|
||||||
vue:
|
|
||||||
optional: true
|
|
||||||
vue-router:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1545,6 +1493,9 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
core-js@3.43.0:
|
||||||
|
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
|
||||||
|
|
||||||
create-jest@29.7.0:
|
create-jest@29.7.0:
|
||||||
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@@ -1880,6 +1831,9 @@ packages:
|
|||||||
fb-watchman@2.0.2:
|
fb-watchman@2.0.2:
|
||||||
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
|
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
|
||||||
|
|
||||||
|
fflate@0.4.8:
|
||||||
|
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -2773,6 +2727,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
posthog-js@1.255.0:
|
||||||
|
resolution: {integrity: sha512-2ZZKrGB1Ih425IoPvmiDYN+BcDJvNJvVGRrey2ARR4UJ85oB+sNCJAx6DuwIlvsIQTe8QjuUhxrHlxAT5/7IMA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@rrweb/types': 2.0.0-alpha.17
|
||||||
|
rrweb-snapshot: 2.0.0-alpha.17
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@rrweb/types':
|
||||||
|
optional: true
|
||||||
|
rrweb-snapshot:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
posthog-node@5.1.1:
|
||||||
|
resolution: {integrity: sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
preact@10.26.9:
|
||||||
|
resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==}
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -3284,6 +3256,9 @@ packages:
|
|||||||
walker@1.0.8:
|
walker@1.0.8:
|
||||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||||
|
|
||||||
|
web-vitals@4.2.4:
|
||||||
|
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4532,16 +4507,6 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.19.1
|
'@typescript-eslint/types': 8.19.1
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
|
||||||
'@vercel/analytics@1.4.1(next@15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
|
||||||
optionalDependencies:
|
|
||||||
next: 15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
|
||||||
react: 19.0.0
|
|
||||||
|
|
||||||
'@vercel/speed-insights@1.1.0(next@15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
|
||||||
optionalDependencies:
|
|
||||||
next: 15.1.4(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
|
||||||
react: 19.0.0
|
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.14.0):
|
acorn-jsx@5.3.2(acorn@8.14.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.14.0
|
acorn: 8.14.0
|
||||||
@@ -4874,6 +4839,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
core-js@3.43.0: {}
|
||||||
|
|
||||||
create-jest@29.7.0(@types/node@22.10.5):
|
create-jest@29.7.0(@types/node@22.10.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
@@ -5351,6 +5318,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bser: 2.1.1
|
bser: 2.1.1
|
||||||
|
|
||||||
|
fflate@0.4.8: {}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
@@ -6418,6 +6387,17 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
posthog-js@1.255.0:
|
||||||
|
dependencies:
|
||||||
|
core-js: 3.43.0
|
||||||
|
fflate: 0.4.8
|
||||||
|
preact: 10.26.9
|
||||||
|
web-vitals: 4.2.4
|
||||||
|
|
||||||
|
posthog-node@5.1.1: {}
|
||||||
|
|
||||||
|
preact@10.26.9: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
pretty-format@29.7.0:
|
pretty-format@29.7.0:
|
||||||
@@ -7005,6 +6985,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
makeerror: 1.0.12
|
makeerror: 1.0.12
|
||||||
|
|
||||||
|
web-vitals@4.2.4: {}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
|
|||||||
121
theme.md
121
theme.md
@@ -1,121 +0,0 @@
|
|||||||
# Violet
|
|
||||||
|
|
||||||
```css
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
--background: 0 0% 100%;
|
|
||||||
--foreground: 224 71.4% 4.1%;
|
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 224 71.4% 4.1%;
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 224 71.4% 4.1%;
|
|
||||||
--primary: 262.1 83.3% 57.8%;
|
|
||||||
--primary-foreground: 210 20% 98%;
|
|
||||||
--secondary: 220 14.3% 95.9%;
|
|
||||||
--secondary-foreground: 220.9 39.3% 11%;
|
|
||||||
--muted: 220 14.3% 95.9%;
|
|
||||||
--muted-foreground: 220 8.9% 46.1%;
|
|
||||||
--accent: 220 14.3% 95.9%;
|
|
||||||
--accent-foreground: 220.9 39.3% 11%;
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
|
||||||
--destructive-foreground: 210 20% 98%;
|
|
||||||
--border: 220 13% 91%;
|
|
||||||
--input: 220 13% 91%;
|
|
||||||
--ring: 262.1 83.3% 57.8%;
|
|
||||||
--radius: 0.65rem;
|
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: 224 71.4% 4.1%;
|
|
||||||
--foreground: 210 20% 98%;
|
|
||||||
--card: 224 71.4% 4.1%;
|
|
||||||
--card-foreground: 210 20% 98%;
|
|
||||||
--popover: 224 71.4% 4.1%;
|
|
||||||
--popover-foreground: 210 20% 98%;
|
|
||||||
--primary: 263.4 70% 50.4%;
|
|
||||||
--primary-foreground: 210 20% 98%;
|
|
||||||
--secondary: 215 27.9% 16.9%;
|
|
||||||
--secondary-foreground: 210 20% 98%;
|
|
||||||
--muted: 215 27.9% 16.9%;
|
|
||||||
--muted-foreground: 217.9 10.6% 64.9%;
|
|
||||||
--accent: 215 27.9% 16.9%;
|
|
||||||
--accent-foreground: 210 20% 98%;
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 210 20% 98%;
|
|
||||||
--border: 215 27.9% 16.9%;
|
|
||||||
--input: 215 27.9% 16.9%;
|
|
||||||
--ring: 263.4 70% 50.4%;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Blue
|
|
||||||
|
|
||||||
```css@layer base {
|
|
||||||
:root {
|
|
||||||
--background: 0 0% 100%;
|
|
||||||
--foreground: 240 10% 3.9%;
|
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 240 10% 3.9%;
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 240 10% 3.9%;
|
|
||||||
--primary: 221.2 83.2% 53.3%;
|
|
||||||
--primary-foreground: 0 0% 98%;
|
|
||||||
--secondary: 240 4.8% 95.9%;
|
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
|
||||||
--muted: 240 4.8% 95.9%;
|
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
|
||||||
--accent: 240 4.8% 95.9%;
|
|
||||||
--accent-foreground: 240 5.9% 10%;
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 240 5.9% 90%;
|
|
||||||
--input: 240 5.9% 90%;
|
|
||||||
--ring: 240 5.9% 10%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: 240 10% 3.9%;
|
|
||||||
--foreground: 0 0% 98%;
|
|
||||||
--card: 240 10% 3.9%;
|
|
||||||
--card-foreground: 0 0% 98%;
|
|
||||||
--popover: 240 10% 3.9%;
|
|
||||||
--popover-foreground: 0 0% 98%;
|
|
||||||
--primary: 217.2 91.2% 59.8%;
|
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
|
||||||
--secondary-foreground: 0 0% 98%;
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 0 0% 98%;
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
|
||||||
--input: 217.2 32.6% 17.5%;
|
|
||||||
--ring: 224.3 76.3% 48%;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
--hover: 240 27% 11%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user