bump, improve/update now playing component to use listenbrainz, update deploy link, update env variables in readme
This commit is contained in:
parent
ffe9419db1
commit
b6b99d26f4
21
README.md
21
README.md
@ -8,15 +8,9 @@ It's built with Next.js and Tailwind CSS. aidxnCC will always be a work in progr
|
|||||||
|
|
||||||
## Deploy with Vercel
|
## Deploy with Vercel
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&env=LASTFM_API_URL&envDescription=LastFM%20public%20API%20link%20for%20your%20username%20(https%3A%2F%2Flastfm-last-played.biancarosa.com.br%2FUSERNAME%2Flatest-song)&envLink=https%3A%2F%2Fgit.pontusmail.org%2Faidan%2FaidxnCC%2Fsrc%2Fbranch%2Fmain%23deploy-with-vercel)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&env=BRAINZ_USER_AGENT,LISTENBRAINZ_TOKEN&envDescription=You%20will%20need%20both%20a%20custom%20user%20agent%20(for%20identifying%20yourself%20to%20MusicBrainz)%2C%20and%20a%20ListenBrainz%20User%20Token.%20See%20the%20README%20for%20more%20information.&envLink=https%3A%2F%2Fgit.pontusmail.org%2Faidan%2FaidxnCC&project-name=aidxn-cc&repository-name=aidxnCC)
|
||||||
|
|
||||||
To deploy with Vercel, simply click the button above. When prompted for `LASTFM_API_URL`, simply input:
|
To deploy with Vercel, simply click the button above. When prompted for environment variables, see the section below.
|
||||||
|
|
||||||
```plaintext
|
|
||||||
https://lastfm-last-played.biancarosa.com.br/USERNAME/latest-song
|
|
||||||
```
|
|
||||||
|
|
||||||
where `USERNAME` is your LastFM username.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@ -24,7 +18,16 @@ Any and all contributions are welcome! Simply create a pull request and I should
|
|||||||
|
|
||||||
Please use common sense when contributing :)
|
Please use common sense when contributing :)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
You will need some environment variables set to properly self-host aidxnCC. They are listed below.
|
||||||
|
|
||||||
|
| Environment Variable | Description | Example |
|
||||||
|
|----------------------|-------------|---------|
|
||||||
|
| `BRAINZ_USER_AGENT` | User agent used to make requests to MusicBrainz (should include your contact info) | `aidxnCC/1.0 ( aidan@p0ntus.com )` |
|
||||||
|
| `LISTENBRAINZ_TOKEN` | Your ListenBrainz user token (get this in [settings](https://listenbrainz.org/settings/)) | `0e0x0a0m-0p0l-0e0t-0o0k-0e0n00000000` |
|
||||||
|
|
||||||
## To-Do
|
## To-Do
|
||||||
|
|
||||||
- [ ] Dockerize for easier deployment
|
- [ ] Dockerize for easier deployment
|
||||||
|
- [ ] Improve speed of fetching now playing
|
||||||
|
47
components/objects/ScrollTxt.tsx
Normal file
47
components/objects/ScrollTxt.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
interface ScrollTxtProps {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollTxt: React.FC<ScrollTxtProps> = ({ text, className = "" }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const textRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [shouldScroll, setShouldScroll] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current && textRef.current) {
|
||||||
|
const containerWidth = containerRef.current.offsetWidth
|
||||||
|
const textWidth = textRef.current.offsetWidth
|
||||||
|
setShouldScroll(textWidth > containerWidth)
|
||||||
|
}
|
||||||
|
}, []) // Updated dependency array
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className={`overflow-hidden ${className}`}>
|
||||||
|
<div
|
||||||
|
ref={textRef}
|
||||||
|
className={`whitespace-nowrap inline-block ${shouldScroll ? "animate-marquee hover:pause" : ""}`}
|
||||||
|
>
|
||||||
|
{shouldScroll ? (
|
||||||
|
<>
|
||||||
|
<span>{text}</span>
|
||||||
|
<span className="mx-4">•</span>
|
||||||
|
<span>{text}</span>
|
||||||
|
<span className="mx-4">•</span>
|
||||||
|
<span>{text}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScrollTxt
|
||||||
|
|
@ -1,64 +1,221 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import type React from "react"
|
||||||
import Image from 'next/image';
|
import { useEffect, useState, useCallback, useRef } from "react"
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import Image from "next/image"
|
||||||
import { faLastfm } from '@fortawesome/free-brands-svg-icons'
|
import { Music, ExternalLink, Disc, User, Loader2 } from "lucide-react"
|
||||||
import { faCompactDisc, faUser } from '@fortawesome/free-solid-svg-icons'
|
import Marquee from "react-fast-marquee"
|
||||||
|
|
||||||
interface Track {
|
interface Track {
|
||||||
name: string;
|
track_name: string
|
||||||
artist: { '#text': string };
|
artist_name: string
|
||||||
album: { '#text': string };
|
release_name?: string
|
||||||
image: { '#text': string; size: string }[];
|
mbid?: string
|
||||||
url: string;
|
|
||||||
'@attr'?: { nowplaying: string };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LastPlayed: React.FC = () => {
|
const ScrollableText: React.FC<{ text: string; className?: string }> = ({ text, className = "" }) => {
|
||||||
const [track, setTrack] = useState<Track | null>(null);
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const apiUrl = process.env.LASTFM_API_URL || 'https://lastfm-last-played.biancarosa.com.br/aidxn_/latest-song';
|
const [shouldScroll, setShouldScroll] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(apiUrl)
|
if (containerRef.current) {
|
||||||
.then(response => response.json())
|
setShouldScroll(containerRef.current.scrollWidth > containerRef.current.clientWidth)
|
||||||
.then(data => setTrack(data.track))
|
console.log("[i] text width checked: ", containerRef.current.scrollWidth, containerRef.current.clientWidth)
|
||||||
.catch(error => console.error('Error fetching now playing:', error));
|
}
|
||||||
}, [apiUrl]);
|
}, [containerRef])
|
||||||
|
|
||||||
if (!track) {
|
if (shouldScroll) {
|
||||||
|
console.log("✅ scrolling is active")
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto mb-12">
|
<Marquee gradientWidth={20} speed={20} pauseOnHover={true}>
|
||||||
<h2 className="text-2xl font-bold mb-4 text-gray-200">Last Played Song</h2>
|
<div className={className}>{text}</div>
|
||||||
<div className="flex justify-center items-center border border-gray-300 rounded-lg p-4 max-w-md mt-8">
|
<span className="mx-4 text-gray-400">•</span>
|
||||||
<span className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full" role="status"></span>
|
</Marquee>
|
||||||
</div>
|
)
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto mb-12">
|
<div ref={containerRef} className={`overflow-hidden ${className}`}>
|
||||||
<h2 className="text-2xl font-bold mb-4 text-gray-200">Last Played Song</h2>
|
<div className="whitespace-nowrap">{text}</div>
|
||||||
<div className="now-playing flex items-center border border-gray-300 rounded-lg p-4 max-w-md mt-8 bg-white/10 backdrop-filter backdrop-blur-lg">
|
</div>
|
||||||
<Image
|
)
|
||||||
src={track.image.find(img => img.size === 'large')?.['#text'] || '/placeholder.png'}
|
}
|
||||||
alt={track.name}
|
|
||||||
width={96}
|
const NowPlaying: React.FC = () => {
|
||||||
height={96}
|
const [track, setTrack] = useState<Track | null>(null)
|
||||||
className="rounded-lg mr-4"
|
const [coverArt, setCoverArt] = useState<string | null>(null)
|
||||||
/>
|
const [loading, setLoading] = useState(true)
|
||||||
<div>
|
const [loadingStatus, setLoadingStatus] = useState("Initializing")
|
||||||
<p className="font-bold">{track.name}</p>
|
const [error, setError] = useState<string | null>(null)
|
||||||
<p><FontAwesomeIcon icon={faCompactDisc} className="mr-1" /> {track.album['#text']}</p>
|
|
||||||
<i><FontAwesomeIcon icon={faUser} className="mr-1" /> {track.artist['#text']}</i>
|
const fetchAlbumArt = useCallback(async (artist: string, album?: string) => {
|
||||||
<a href={track.url} target="_blank" rel="noopener noreferrer" className="text-blue-500 flex items-center">
|
if (!album) {
|
||||||
<FontAwesomeIcon icon={faLastfm} className="mr-2" /> View on Last.fm
|
console.log("[i] no album found")
|
||||||
|
setCoverArt(null)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log("[i] fetching album art: ", artist, album)
|
||||||
|
const response = await fetch(
|
||||||
|
`https://musicbrainz.org/ws/2/release/?query=artist:${encodeURIComponent(
|
||||||
|
artist
|
||||||
|
)}%20AND%20release:${encodeURIComponent(album)}&fmt=json`
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log("[!] album art fetch error:", response.status)
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.releases && data.releases.length > 0) {
|
||||||
|
const mbid = data.releases[0].id
|
||||||
|
console.log("✅ mbid found:", mbid)
|
||||||
|
setTrack(prev => prev ? { ...prev, mbid: `${mbid || null}` } : { track_name: "", artist_name: "", release_name: undefined, mbid: `${mbid || null}` })
|
||||||
|
const coverArtResponse = await fetch(`https://coverartarchive.org/release/${mbid}/front-250`)
|
||||||
|
if (coverArtResponse.ok) {
|
||||||
|
console.log("✅ cover art found")
|
||||||
|
setCoverArt(coverArtResponse.url)
|
||||||
|
} else {
|
||||||
|
console.log("[i] cover art not found!")
|
||||||
|
setCoverArt(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[i] no releases in data!")
|
||||||
|
setCoverArt(null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[!] album art error", error)
|
||||||
|
setCoverArt(null)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
console.log("[i] album art done")
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchNowPlaying = useCallback(async () => {
|
||||||
|
setLoadingStatus("Fetching now playing")
|
||||||
|
console.log("[i] fetching now playing...")
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.listenbrainz.org/1/user/p0ntus/playing-now", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${process.env.NEXT_PUBLIC_LISTENBRAINZ_TOKEN}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log("[!] now playing error:", response.status)
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.payload.count > 0 && data.payload.listens[0].track_metadata) {
|
||||||
|
const trackMetadata = data.payload.listens[0].track_metadata
|
||||||
|
console.log("✅ track found: ", trackMetadata.track_name)
|
||||||
|
console.log("[i] song details: ", trackMetadata.track_name, "-", trackMetadata.artist_name, "/", trackMetadata.release_name)
|
||||||
|
setTrack({
|
||||||
|
track_name: trackMetadata.track_name,
|
||||||
|
artist_name: trackMetadata.artist_name,
|
||||||
|
release_name: trackMetadata.release_name,
|
||||||
|
mbid: trackMetadata.mbid,
|
||||||
|
})
|
||||||
|
setLoadingStatus("Fetching album art")
|
||||||
|
await fetchAlbumArt(trackMetadata.artist_name, trackMetadata.release_name)
|
||||||
|
} else {
|
||||||
|
console.log("[i] no track playing")
|
||||||
|
setLoadingStatus("No track playing")
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("[!] now playing error:", error)
|
||||||
|
setError("Error fetching now playing data")
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [fetchAlbumArt])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNowPlaying()
|
||||||
|
}, [fetchNowPlaying])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-200">Now Playing</h2>
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<Loader2 className="animate-spin text-gray-200" size={24} />
|
||||||
|
<p className="text-gray-200">{loadingStatus}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-200">Now Playing</h2>
|
||||||
|
<div className="flex items-center justify-center text-red-500">
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-200">Now Playing</h2>
|
||||||
|
<div className="flex items-center justify-center text-gray-200">
|
||||||
|
<p>No track playing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-200">Now Playing</h2>
|
||||||
|
<div className="now-playing flex items-center border border-gray-300 rounded-lg p-4 bg-white/10 backdrop-filter backdrop-blur-lg shadow-lg">
|
||||||
|
{coverArt ? (
|
||||||
|
<div className="relative w-24 h-24 rounded-lg mr-4 flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={coverArt || ""}
|
||||||
|
alt={track.track_name}
|
||||||
|
fill
|
||||||
|
sizes="96px"
|
||||||
|
style={{ objectFit: "cover" }}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-24 h-24 bg-gray-200 rounded-lg mr-4 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Music size={48} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-grow min-w-0 overflow-hidden">
|
||||||
|
<div className="flex items-center space-x-2 font-bold text-lg mb-1">
|
||||||
|
<Music size={16} className="text-gray-200 flex-shrink-0" />
|
||||||
|
<ScrollableText text={track.track_name} className="text-gray-200" />
|
||||||
|
</div>
|
||||||
|
{track.release_name && (
|
||||||
|
<div className="flex items-center space-x-2 mb-1">
|
||||||
|
<Disc size={16} className="text-gray-300 flex-shrink-0" />
|
||||||
|
<ScrollableText text={track.release_name} className="text-gray-300" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<User size={16} className="text-gray-300 flex-shrink-0" />
|
||||||
|
<ScrollableText text={track.artist_name} className="text-gray-300" />
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={track.mbid ? `https://musicbrainz.org/release/${track.mbid}` : `https://listenbrainz.org/user/p0ntus`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-400 flex items-center mt-1 hover:text-blue-300 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<ExternalLink size={16} className="mr-1 flex-shrink-0" />
|
||||||
|
<span>View on MusicBrainz</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LastPlayed;
|
export default NowPlaying
|
@ -3,12 +3,6 @@ import type { NextConfig } from "next";
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: 'lastfm.freetls.fastly.net',
|
|
||||||
port: '',
|
|
||||||
pathname: '/**',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'p0ntus.com',
|
hostname: 'p0ntus.com',
|
||||||
@ -21,10 +15,15 @@ const nextConfig: NextConfig = {
|
|||||||
port: '',
|
port: '',
|
||||||
pathname: '/**',
|
pathname: '/**',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.archive.org',
|
||||||
|
port: '',
|
||||||
|
pathname: '/**',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
dangerouslyAllowSVG: true,
|
dangerouslyAllowSVG: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
@ -17,18 +17,20 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "^15.2.0-canary.63",
|
"next": "^15.2.0-canary.63",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-fast-marquee": "^1.6.5",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/postcss": "^4.0.6",
|
||||||
"@types/node": "^20.17.19",
|
"@types/node": "^20.17.19",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"eslint": "^9.20.1",
|
"eslint": "^9.20.1",
|
||||||
"eslint-config-next": "15.1.3",
|
"eslint-config-next": "15.1.3",
|
||||||
"postcss": "^8.5.2",
|
"postcss": "^8.5.2",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.6",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
65
tailwind.config.ts
Normal file
65
tailwind.config.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { Config } from "tailwindcss"
|
||||||
|
import tailwindAnimate from "tailwindcss-animate"
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: "class",
|
||||||
|
content: [
|
||||||
|
"app/**/*.{ts,tsx}",
|
||||||
|
"components/**/*.{ts,tsx}",
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
neon: '0 0 5px theme("colors.blue.400"), 0 0 20px theme("colors.blue.700")',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [tailwindAnimate],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
|
Reference in New Issue
Block a user