feat: add spotify widget
This commit is contained in:
parent
40927e3cda
commit
231488859c
8 changed files with 168 additions and 5 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
SPOTIFY_CLIENT_ID=
|
||||||
|
SPOTIFY_CLIENT_SECRET=
|
||||||
|
SPOTIFY_REFRESH_TOKEN=
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -34,4 +34,5 @@ yarn-error.log*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import ReactFullpage from "@fullpage/react-fullpage";
|
import ReactFullpage from "@fullpage/react-fullpage";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import "../globals.css";
|
// import "../globals.css";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|
|
||||||
25
app/about/(spotify)/animation.jsx
Normal file
25
app/about/(spotify)/animation.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
const PlayingAnimation = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative flex justify-between w-4 h-8 space-x-1">
|
||||||
|
<motion.span
|
||||||
|
className="w-1 h-full bg-gray-500 rounded-full"
|
||||||
|
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||||
|
transition={{ duration: 2.2, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<motion.span
|
||||||
|
className="w-1 h-full bg-gray-500 rounded-full"
|
||||||
|
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||||
|
transition={{ duration: 2.2, repeat: Infinity, delay: -2 }}
|
||||||
|
/>
|
||||||
|
<motion.span
|
||||||
|
className="w-1 h-full bg-gray-500 rounded-full"
|
||||||
|
animate={{ scaleY: [0.3, 1, 0.5, 0.75, 0.5] }}
|
||||||
|
transition={{ duration: 2.2, repeat: Infinity, delay: -3.7 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlayingAnimation;
|
||||||
78
app/about/(spotify)/card.jsx
Normal file
78
app/about/(spotify)/card.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import getNowPlayingItem from "./fetch";
|
||||||
|
import PlayingAnimation from "./animation";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faSpotify } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const Card = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [result, setResult] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const results = await Promise.all([getNowPlayingItem()]);
|
||||||
|
console.log(results);
|
||||||
|
setResult(results[0]);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
fetchData(); // Fetch data immediately on component mount
|
||||||
|
|
||||||
|
const intervalId = setInterval(fetchData, 60 * 1000); // ! Fetch data every minute: temporary
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 flex justify-center w-full">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
{/* Replace this with your own loading spinner */}
|
||||||
|
<div className="loader ease-linear rounded-full border-4 border-t-4 border-black h-12 w-12 mb-4"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative w-full mb-8 grid grid-cols-2 gap-2 border-2 border-gray-500 p-4 rounded-lg backdrop-filter backdrop-blur-lg bg-white bg-opacity-30">
|
||||||
|
<Image
|
||||||
|
src={result.albumImageUrl}
|
||||||
|
alt="backgroundImage"
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
className="z-0 opacity-20 absolute"
|
||||||
|
/>
|
||||||
|
<div className="z-10 flex items-center">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSpotify}
|
||||||
|
className="text-black text-5xl me-5 md:me-1 xl:me-8"
|
||||||
|
/>
|
||||||
|
<p className="font-semibold me-5 md:me-1 xl:me-7">
|
||||||
|
{result.isPlaying
|
||||||
|
? "Now playing"
|
||||||
|
: "Currently offline"}
|
||||||
|
</p>
|
||||||
|
{result.isPlaying && <PlayingAnimation />}
|
||||||
|
</div>
|
||||||
|
{result.isPlaying && (
|
||||||
|
<div className="z-10 rounded-lg ms-5">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<a
|
||||||
|
href={result.songUrl}
|
||||||
|
target="_blank"
|
||||||
|
className="block font-semibold w-full truncate text-blue-500">
|
||||||
|
{result.title}
|
||||||
|
</a>
|
||||||
|
<p className="truncate text-gray-500">
|
||||||
|
{result.artist}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
||||||
|
|
||||||
53
app/about/(spotify)/fetch.js
Normal file
53
app/about/(spotify)/fetch.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import querystring from "querystring";
|
||||||
|
const TOKEN_ENDPOINT = `https://accounts.spotify.com/api/token`;
|
||||||
|
const NOW_PLAYING_ENDPOINT = `https://api.spotify.com/v1/me/player/currently-playing`;
|
||||||
|
const client_id = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_ID;
|
||||||
|
const client_secret = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_SECRET;
|
||||||
|
const refresh_token = process.env.NEXT_PUBLIC_SPOTIFY_REFRESH_TOKEN;
|
||||||
|
|
||||||
|
const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
|
||||||
|
const getAccessToken = async () => {
|
||||||
|
const response = await fetch(TOKEN_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${basic}`,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: querystring.stringify({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNowPlaying = async () => {
|
||||||
|
const { access_token } = await getAccessToken();
|
||||||
|
return fetch(NOW_PLAYING_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getNowPlayingItem() {
|
||||||
|
const response = await getNowPlaying();
|
||||||
|
if (response.status === 204 || response.status > 400) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const song = await response.json();
|
||||||
|
const albumImageUrl = song.item.album.images[0].url;
|
||||||
|
const artist = song.item.artists.map((_artist) => _artist.name).join(", ");
|
||||||
|
const isPlaying = song.is_playing;
|
||||||
|
const songUrl = song.item.external_urls.spotify;
|
||||||
|
const title = song.item.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
albumImageUrl,
|
||||||
|
artist,
|
||||||
|
isPlaying,
|
||||||
|
songUrl,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
|
|
@ -8,19 +7,19 @@ import FixedButon from "@/components/FixedButton";
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Card from "./(spotify)/card.jsx";
|
||||||
|
|
||||||
// images
|
// images
|
||||||
import Me1 from "@/public/img/me1.jpg";
|
import Me1 from "@/public/img/me1.jpg";
|
||||||
import Me2 from "@/public/img/me2.jpg";
|
import Me2 from "@/public/img/me2.jpg";
|
||||||
import Me3 from "@/public/img/me3.jpg";
|
import Me3 from "@/public/img/me3.jpg";
|
||||||
import Hr from "@/components/Hr";
|
import Hr from "@/components/Hr";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
@ -207,6 +206,7 @@ export default function Page() {
|
||||||
connect and explore this ever-evolving world of tech
|
connect and explore this ever-evolving world of tech
|
||||||
together!
|
together!
|
||||||
</p>
|
</p>
|
||||||
|
<Card />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
<div className=" w-screen mx-auto container gap-4 p-10 grid grid-cols-1 md:grid-cols-2 mt-10 mb-24">
|
<div className=" w-screen mx-auto container gap-4 p-10 grid grid-cols-1 md:grid-cols-2 mt-10 mb-24">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
});
|
});
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
// your Next.js configuration
|
// your Next.js configuration
|
||||||
|
images: {
|
||||||
|
domains: ["i.scdn.co"],
|
||||||
|
},
|
||||||
webpack: (config, options) => {
|
webpack: (config, options) => {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.pdf$/i,
|
test: /\.pdf$/i,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue